mlbserver 2025.4.2 → 2025.4.5

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 -2
  2. package/index.js +45 -22
  3. package/package.json +1 -1
  4. package/session.js +265 -77
package/README.md CHANGED
@@ -43,7 +43,7 @@ After launching the server or Docker container, you can access it at http://loca
43
43
  Basic command line or Docker environment options:
44
44
 
45
45
  ```
46
- --port or -p (primary port to run on; defaults to 9999 if not specified)
46
+ --port or -p (primary port to run on; defaults to 9999 in Docker or if not specified)
47
47
  --debug or -d (false if not specified)
48
48
  --version or -v (returns package version number)
49
49
  --logout or -l (logs out and clears session)
@@ -74,7 +74,7 @@ Advanced command line or Docker environment options:
74
74
 
75
75
  Supports [SWAG](https://docs.linuxserver.io/general/swag/#preset-proxy-confs) using the custom [mlbserver.subfolder.conf](https://github.com/tonywagner/mlbserver/blob/master/mlbserver.subfolder.conf) file.
76
76
 
77
- For multiview, the default software encoder is limited by your CPU. You may want to experiment with different ffmpeg hardware encoders. "h264_videotoolbox" is confirmed to work on supported Macs, and "h264_v4l2m2m" is confirmed to work on a Raspberry Pi 4 (and likely other Linux systems) when ffmpeg is compiled with this patch: https://www.raspberrypi.org/forums/viewtopic.php?p=1780625#p1780625
77
+ For multiview, the default software video encoder is limited by your CPU. You may want to experiment with different ffmpeg hardware video encoders. "h264_videotoolbox" is confirmed to work on supported Macs, and "h264_v4l2m2m" is confirmed to work on a Raspberry Pi 4 (and likely other Linux systems) when ffmpeg is compiled with this patch: https://www.raspberrypi.org/forums/viewtopic.php?p=1780625#p1780625
78
78
 
79
79
  More potential hardware encoders are described at https://stackoverflow.com/a/50703794
80
80
 
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">/slashes/ indicates a live pre- and/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
 
@@ -2299,7 +2317,9 @@ app.get('/', async function(req, res) {
2299
2317
 
2300
2318
  body += '<p><span class="tooltip">Include by level<span class="tooltiptext">Including a level (AAA, AA, A+ encoded as A%2B, or A, in a comma-separated list if more than 1) will include all of its broadcasts, and exclude all other levels.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeLevels=a%2B,aaa' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeLevels=a%2B,aaa' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeLevels=a%2B,aaa' + content_protect_b + '">ics</a></p>' + "\n"
2301
2319
 
2302
- 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"
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. A value of "channels" will format the titles in the style of the <a href="https://community.getchannels.com/t/mlb-tv-for-channels/27492">legacy Channels container</a>.</span></span>: <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">guide.xml</a> or <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=channels' + content_protect_b + '">legacy</a>, and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">calendar.ics</a></p>' + "\n"
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 game on that channel. A value of "channels" will format the events in the style of the <a href="https://community.getchannels.com/t/mlb-tv-for-channels/27492">legacy Channels container</a>.</span></span>: <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&offAir=true' + content_protect_b + '">guide.xml</a> or <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&offAir=channels' + content_protect_b + '">legacy</a></p>' + "\n"
2303
2323
 
2304
2324
  body += '</td></tr></table><br/>' + "\n"
2305
2325
 
@@ -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.05",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -34,6 +34,8 @@ const AFL_ID = '119'
34
34
  const LIDOM_ID = '131'
35
35
  const WINTER_LEAGUES = [AFL_ID, LIDOM_ID]
36
36
 
37
+ const OFF_AIR_LOGO = 'https://lh3.googleusercontent.com/uVJBX-jpgwHsDY_o6-po2JU5-cDZuoq_CsCcqJ0-T7996z8NbOzeQCfQaAG0DB2hbkxv2VvtZ2E'
38
+
37
39
  // These are the events to ignore, if we're skipping breaks
38
40
  const BREAK_TYPES = ['Game Advisory', 'Pitching Substitution', 'Offensive Substitution', 'Defensive Sub', 'Defensive Switch', 'Runner Placed On Base']
39
41
  // These are the events to keep, in addition to the last event of each at-bat, if we're skipping pitches
@@ -44,10 +46,10 @@ const EVENT_START_PADDING = -3
44
46
  const PITCH_END_PADDING = 2
45
47
  const ACTION_END_PADDING = 7
46
48
  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
49
+ // hardcode extra padding for MLB events
50
+ const MLB_PADDING = 2
51
+ // hardcode extra Game Changer padding for MLB, in 10-second increments as needed
52
+ const MLB_GAMECHANGER_PADDING = 0
51
53
 
52
54
  const LI_TABLE = {
53
55
  1: {
@@ -2158,7 +2160,7 @@ class sessionClass {
2158
2160
  }
2159
2161
 
2160
2162
  // 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) {
2163
+ async getTVData(dataType, mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, includeTeamsInTitles='false', offAir='false', resolution='best', pipe='false', startingChannelNumber=1) {
2162
2164
  try {
2163
2165
  this.debuglog('getTVData for ' + dataType)
2164
2166
 
@@ -2247,6 +2249,8 @@ class sessionClass {
2247
2249
 
2248
2250
  let blackouts = {}
2249
2251
  if ( includeBlackouts == 'false' ) blackouts = await this.get_blackout_games()
2252
+
2253
+ let pre_post_shows = await this.get_pre_post_shows()
2250
2254
 
2251
2255
  for (var i = 0; i < cache_data.dates.length; i++) {
2252
2256
  this.debuglog('getTVData processing date ' + cache_data.dates[i].date)
@@ -2301,29 +2305,35 @@ class sessionClass {
2301
2305
  stream += '&resolution=' + resolution
2302
2306
  if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
2303
2307
  if ( pipe == 'true' ) stream = await this.convert_stream_to_pipe(stream, channelid)
2304
- channels[channelid] = await this.create_channel_object(channelid, logo, stream, mediaType)
2308
+ if ( !channels[channelid] ) {
2309
+ channels[channelid] = await this.create_channel_object(channelid, logo, stream, mediaType)
2310
+ }
2305
2311
 
2306
2312
  let title = 'Minor League Baseball'
2307
2313
  if ( WINTER_LEAGUES.includes(league_id.toString()) ) {
2308
2314
  title = cache_data.dates[i].games[j].teams['home'].team.league.name
2309
2315
  }
2310
2316
 
2311
- let away_team = cache_data.dates[i].games[j].teams['away'].team.name
2312
- let home_team = cache_data.dates[i].games[j].teams['home'].team.name
2317
+ let away_team = cache_data.dates[i].games[j].teams['away'].team.shortName
2318
+ let home_team = cache_data.dates[i].games[j].teams['home'].team.shortName
2313
2319
  let subtitle = away_team + ' at ' + home_team
2314
-
2315
- if (includeTeamsInTitles == 'true') {
2316
- if ( league_id == AFL_ID ) {
2317
- title = 'AFL'
2318
- } else if ( league_id == LIDOM_ID ) {
2319
- title = 'LIDOM'
2320
+
2321
+ if ( includeTeamsInTitles != 'false' ) {
2322
+ if ( includeTeamsInTitles == 'channels' ) {
2323
+ title = this.channelsFormattedTitle(subtitle, cache_data.dates[i].games[j].gameDate)
2320
2324
  } else {
2321
- title = 'MiLB'
2325
+ if ( league_id == AFL_ID ) {
2326
+ title = 'AFL'
2327
+ } else if ( league_id == LIDOM_ID ) {
2328
+ title = 'LIDOM'
2329
+ } else {
2330
+ title = 'MiLB'
2331
+ }
2332
+ title += ': ' + subtitle
2322
2333
  }
2323
- title += ': ' + subtitle
2324
2334
  }
2325
2335
 
2326
- let description = cache_data.dates[i].games[j].teams['home'].team.league.name + '. '
2336
+ let description = cache_data.dates[i].games[j].teams['home'].team.sport.name + ' ' + cache_data.dates[i].games[j].teams['home'].team.league.name + '. '
2327
2337
  if ( cache_data.dates[i].games[j].seriesDescription != 'Regular Season' ) {
2328
2338
  description += cache_data.dates[i].games[j].seriesDescription + '. '
2329
2339
  }
@@ -2334,6 +2344,9 @@ class sessionClass {
2334
2344
  if ( scheduledInnings != '9' ) {
2335
2345
  description += scheduledInnings + '-inning game. '
2336
2346
  }
2347
+ if ( cache_data.dates[i].games[j].teams['away'].team.parentOrgName && cache_data.dates[i].games[j].teams['home'].team.parentOrgName ) {
2348
+ description += cache_data.dates[i].games[j].teams['away'].team.name + ' (' + this.getParent(cache_data.dates[i].games[j].teams['away'].team.parentOrgName) + ') at ' + cache_data.dates[i].games[j].teams['home'].team.name + ' (' + this.getParent(cache_data.dates[i].games[j].teams['home'].team.parentOrgName) + '). '
2349
+ }
2337
2350
  if ( (cache_data.dates[i].games[j].teams['away'].probablePitcher && cache_data.dates[i].games[j].teams['away'].probablePitcher.fullName) || (cache_data.dates[i].games[j].teams['home'].probablePitcher && cache_data.dates[i].games[j].teams['home'].probablePitcher.fullName) ) {
2338
2351
  if ( cache_data.dates[i].games[j].teams['away'].probablePitcher && cache_data.dates[i].games[j].teams['away'].probablePitcher.fullName ) {
2339
2352
  description += cache_data.dates[i].games[j].teams['away'].probablePitcher.fullName
@@ -2380,21 +2393,22 @@ class sessionClass {
2380
2393
  let location = server + '/embed.html?team=' + encodeURIComponent(team) + '&mediaType=' + streamMediaType
2381
2394
  if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
2382
2395
  calendar += await this.generate_ics_event(prefix, calendar_start, calendar_stop, subtitle, description, location)
2396
+
2397
+ // Off Air if necessary
2398
+ let off_air_event = await this.generate_off_air_event(offAir, channelid, cache_data.dates[i].date, channels[channelid].stop, cache_data.dates[i].games[j].gameDate, cache_data.dates[i].games[j].teams['away'].team.shortName + ' at ' + cache_data.dates[i].games[j].teams['home'].team.shortName)
2399
+ if ( off_air_event ) {
2400
+ programs += off_air_event
2401
+ channels[channelid].stop = stop
2402
+ }
2383
2403
 
2384
2404
  // 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)
2405
+ 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
2406
 
2387
2407
  //break
2388
2408
  //}
2389
2409
  }
2390
2410
  } else {
2391
2411
  // 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
2412
  if ( cache_data.dates[i].games[j].broadcasts ) {
2399
2413
  // initial loop will count number of broadcasts
2400
2414
  let broadcast_count = await this.count_broadcasts(cache_data.dates[i].games[j].broadcasts, mediaType, language)
@@ -2404,10 +2418,14 @@ class sessionClass {
2404
2418
  let mediaTitle = 'Audio'
2405
2419
  if ( broadcast.type == 'TV' ) {
2406
2420
  mediaTitle = 'MLBTV'
2407
- } else if ( broadcast.language == 'es' ) {
2408
- mediaTitle = 'Spanish'
2409
2421
  }
2410
2422
  if ( mediaType == mediaTitle ) {
2423
+
2424
+ // check blackout or non-entitlement status, if necessary
2425
+ let gamePk = cache_data.dates[i].games[j].gamePk.toString()
2426
+ if ( (includeBlackouts == 'false') && blackouts[gamePk] && blackouts[gamePk].blackout_feeds && blackouts[gamePk].blackout_feeds.includes(broadcast.mediaId) ) {
2427
+ continue
2428
+ }
2411
2429
 
2412
2430
  if ( (broadcast.type == 'TV') || ((mediaType == 'Audio') && (broadcast.language == language)) ) {
2413
2431
  let teamType = broadcast.homeAway
@@ -2486,13 +2504,17 @@ class sessionClass {
2486
2504
  //logo += '/image.svg?teamId=MLB'
2487
2505
  //if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2488
2506
  logo = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRi5AKF6eAu9Va9BzZzgw0PSsQXw8rXPiQLHA'
2489
- nationalChannels[channelid] = await this.create_channel_object(channelid, logo, stream, channelMediaType)
2507
+ if ( !nationalChannels[channelid] ) {
2508
+ nationalChannels[channelid] = await this.create_channel_object(channelid, logo, stream, channelMediaType)
2509
+ }
2490
2510
  } else {
2491
2511
  seriesId = cache_data.dates[i].games[j].teams[teamType].team.id
2492
2512
  //logo += '/image.svg?teamId=' + cache_data.dates[i].games[j].teams[teamType].team.id
2493
2513
  //if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2494
2514
  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)
2515
+ if ( !channels[channelid] ) {
2516
+ channels[channelid] = await this.create_channel_object(channelid, logo, stream, channelMediaType)
2517
+ }
2496
2518
  }
2497
2519
 
2498
2520
  let title = 'MLB Baseball'
@@ -2501,15 +2523,19 @@ class sessionClass {
2501
2523
  let home_team = cache_data.dates[i].games[j].teams['home'].team.teamName
2502
2524
  let subtitle = away_team + ' at ' + home_team
2503
2525
 
2504
- if (includeTeamsInTitles == 'true') {
2505
- title = 'MLB: ' + subtitle + ' (' + station
2506
- if ( language == 'es' ) {
2507
- title += ' Spanish'
2508
- }
2509
- if ( mediaType == 'Audio' ) {
2510
- title += ' Radio'
2526
+ if (includeTeamsInTitles != 'false') {
2527
+ if ( includeTeamsInTitles == 'channels' ) {
2528
+ title = this.channelsFormattedTitle(subtitle, cache_data.dates[i].games[j].gameDate)
2529
+ } else {
2530
+ title = 'MLB: ' + subtitle + ' (' + station
2531
+ if ( language == 'es' ) {
2532
+ title += ' Spanish'
2533
+ }
2534
+ if ( mediaType == 'Audio' ) {
2535
+ title += ' Radio'
2536
+ }
2537
+ title += ')'
2511
2538
  }
2512
- title += ')'
2513
2539
  }
2514
2540
 
2515
2541
  let description = station
@@ -2585,7 +2611,42 @@ class sessionClass {
2585
2611
  calendar += await this.generate_ics_event(prefix, calendar_start, calendar_stop, subtitle, description, location)
2586
2612
 
2587
2613
  // 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)
2614
+ 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)
2615
+
2616
+ // pre- and post-game shows
2617
+ if ( pre_post_shows.pregame_shows[broadcast.mediaId] || pre_post_shows.postgame_shows[broadcast.mediaId] ) {
2618
+ 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')) ) {
2619
+ away_team = false
2620
+ } else {
2621
+ home_team = false
2622
+ }
2623
+ // pre-game
2624
+ if ( pre_post_shows.pregame_shows[broadcast.mediaId] ) {
2625
+ let pre_start = this.convertDateToXMLTV(new Date(pre_post_shows.pregame_shows[broadcast.mediaId].start))
2626
+ title = cache_data.dates[i].games[j].teams[pre_post_shows.pregame_shows[broadcast.mediaId].team].team.teamName + ' Pregame'
2627
+ let preSeriesId = seriesId + '1'
2628
+ 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)
2629
+ start = pre_start
2630
+ }
2631
+ // post-game
2632
+ if ( pre_post_shows.postgame_shows[broadcast.mediaId] ) {
2633
+ let postgameMinutes = 30
2634
+ let startDate = stopDate
2635
+ stopDate.setMinutes(stopDate.getMinutes()+postgameMinutes)
2636
+ let post_stop = this.convertDateToXMLTV(stopDate)
2637
+ title = cache_data.dates[i].games[j].teams[pre_post_shows.postgame_shows[broadcast.mediaId].team].team.teamName + ' Postgame'
2638
+ let postSeriesId = seriesId + '2'
2639
+ 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)
2640
+ stop = post_stop
2641
+ }
2642
+ }
2643
+
2644
+ // Off Air if necessary
2645
+ let off_air_event = await this.generate_off_air_event(offAir, channelid, cache_data.dates[i].date, channels[channelid].stop, cache_data.dates[i].games[j].gameDate, subtitle)
2646
+ if ( off_air_event ) {
2647
+ programs += off_air_event
2648
+ channels[channelid].stop = stop
2649
+ }
2589
2650
  }
2590
2651
  }
2591
2652
  }
@@ -2599,7 +2660,6 @@ class sessionClass {
2599
2660
  }
2600
2661
  channels = this.sortObj(channels)
2601
2662
  channels = Object.assign(channels, nationalChannels)
2602
-
2603
2663
 
2604
2664
  let entitlements = await this.getEntitlements()
2605
2665
  // MLB Network live stream for eligible USA subscribers
@@ -2699,7 +2759,7 @@ class sessionClass {
2699
2759
  }
2700
2760
 
2701
2761
  // Big Inning
2702
- if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2762
+ if ( (entitlements.length > 0) && (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2703
2763
  if ( (excludeTeams.length > 0) && excludeTeams.includes('BIGINNING') ) {
2704
2764
  // do nothing
2705
2765
  } else if ( (includeTeams.length == 0) || includeTeams.includes('BIGINNING') ) {
@@ -2733,6 +2793,13 @@ class sessionClass {
2733
2793
  let location = server + '/embed.html?event=biginning&mediaType=Video&resolution=' + resolution
2734
2794
  if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
2735
2795
  calendar += await this.generate_ics_event(prefix, new Date(this.cache.bigInningSchedule[gameDate].start), new Date(this.cache.bigInningSchedule[gameDate].end), title, description, location)
2796
+
2797
+ // Off Air if necessary
2798
+ let off_air_event = await this.generate_off_air_event(offAir, channelid, gameDate, channels[channelid].stop, this.cache.bigInningSchedule[gameDate].start, title)
2799
+ if ( off_air_event ) {
2800
+ programs += off_air_event
2801
+ channels[channelid].stop = stop
2802
+ }
2736
2803
 
2737
2804
  // Big Inning guide XML
2738
2805
  programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(new Date(this.cache.bigInningSchedule[gameDate].start)))
@@ -2750,7 +2817,7 @@ class sessionClass {
2750
2817
  }
2751
2818
 
2752
2819
  // Game Changer
2753
- if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2820
+ if ( (entitlements.length > 0) && (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2754
2821
  if ( (excludeTeams.length > 0) && excludeTeams.includes('GAMECHANGER') ) {
2755
2822
  // do nothing
2756
2823
  } else if ( (includeTeams.length == 0) || includeTeams.includes('GAMECHANGER') ) {
@@ -2785,6 +2852,13 @@ class sessionClass {
2785
2852
  let location = server + '/embed.html?src=' + encodeURIComponent(stream)
2786
2853
  if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
2787
2854
  calendar += await this.generate_ics_event(prefix, new Date(cache_data.dates[i].games[gameIndexes.firstGameIndex].gameDate), gameDate, title, description, location)
2855
+
2856
+ // Off Air if necessary
2857
+ let off_air_event = await this.generate_off_air_event(offAir, channelid, cache_data.dates[i].date, channels[channelid].stop, cache_data.dates[i].games[gameIndexes.firstGameIndex].gameDate, title)
2858
+ if ( off_air_event ) {
2859
+ programs += off_air_event
2860
+ channels[channelid].stop = stop
2861
+ }
2788
2862
 
2789
2863
  // Game Changer guide XML
2790
2864
  programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(gameDate))
@@ -2797,7 +2871,7 @@ class sessionClass {
2797
2871
  }
2798
2872
 
2799
2873
  // Multiview
2800
- if ( (mediaType == 'MLBTV') && (typeof this.data.multiviewStreamURLPath !== 'undefined') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2874
+ if ( (entitlements.length > 0) && (mediaType == 'MLBTV') && (typeof this.data.multiviewStreamURLPath !== 'undefined') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2801
2875
  if ( (excludeTeams.length > 0) && excludeTeams.includes('MULTIVIEW') ) {
2802
2876
  // do nothing
2803
2877
  } else if ( (includeTeams.length == 0) || includeTeams.includes('MULTIVIEW') ) {
@@ -2831,6 +2905,13 @@ class sessionClass {
2831
2905
  let prefix = 'Watch'
2832
2906
  let location = stream.replace('/stream.m3u8?src=', '/embed.html?msrc=')
2833
2907
  calendar += await this.generate_ics_event(prefix, new Date(cache_data.dates[i].games[gameIndexes.firstGameIndex].gameDate), gameDate, title, description, location)
2908
+
2909
+ // Off Air if necessary
2910
+ let off_air_event = await this.generate_off_air_event(offAir, channelid, cache_data.dates[i].date, channels[channelid].stop, cache_data.dates[i].games[gameIndexes.firstGameIndex].gameDate, title)
2911
+ if ( off_air_event ) {
2912
+ programs += off_air_event
2913
+ channels[channelid].stop = stop
2914
+ }
2834
2915
 
2835
2916
  // Multview guide XML
2836
2917
  programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(gameDate))
@@ -2841,7 +2922,7 @@ class sessionClass {
2841
2922
  this.debuglog('getTVData completed Multiview')
2842
2923
  }
2843
2924
  }
2844
- }
2925
+ }
2845
2926
  } catch(e) {
2846
2927
  this.log('getTVData processing error : ' + e.message)
2847
2928
  }
@@ -3659,40 +3740,57 @@ class sessionClass {
3659
3740
 
3660
3741
  let cache_data
3661
3742
  cache_data = await this.getBlackoutsData(gameDate)
3743
+
3744
+ let feedTypes = ['videoFeeds', 'audioFeeds']
3662
3745
 
3663
3746
  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]
3747
+ for (var i = 0; i < cache_data.results.length; i++) {
3748
+ let game = cache_data.results[i]
3666
3749
  let game_pk = game.gamePk
3667
3750
  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')
3751
+ let blackout_type = ''
3752
+ if ( game.blackedOutVideo || !game.entitledVideo || !game.entitledAudio ) {
3753
+ if ( !game.entitledVideo || !game.entitledAudio ) {
3754
+ this.debuglog('get_blackout_games found non-entitled game ' + game_pk)
3679
3755
  blackout_type = 'Not entitled'
3756
+ } else {
3757
+ this.debuglog('get_blackout_games found blackout game ' + game_pk)
3680
3758
  }
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
-
3759
+ blackouts[game_pk] = { blackout_type: blackout_type }
3760
+ }
3761
+
3762
+ let blackout_feeds = []
3763
+ for (var j = 0; j < feedTypes.length; j++) {
3764
+ let feedType = feedTypes[j]
3765
+ for (var k = 0; k < game[feedType].length; k++) {
3766
+ let feed = game[feedType][k]
3767
+ if ( !feed.entitled || ((j == 0) && feed.blackedOut) ) {
3768
+ blackout_feeds.push(feed['mediaId'])
3769
+ if ( !feed['entitled'] ) {
3770
+ this.debuglog('get_blackout_games found non-entitled feed ' + feed.callLetters)
3771
+ blackout_type = 'Not entitled'
3772
+ } else {
3773
+ this.debuglog('get_blackout_games found blackout feed ' + feed.callLetters)
3774
+ }
3775
+ }
3776
+ }
3777
+ }
3778
+ if ( blackout_feeds.length > 0 ) {
3779
+ if ( !blackouts[game_pk] ) {
3780
+ blackouts[game_pk] = { blackout_type: blackout_type }
3781
+ }
3782
+ blackouts[game_pk].blackout_feeds = blackout_feeds
3783
+ }
3784
+
3687
3785
  // add blackout expiry, if requested
3688
3786
  if ( blackouts[game_pk] && (blackouts[game_pk].blackout_type != 'Not entitled') && calculate_expiries && await this.check_game_time(game.gameData) ) {
3689
3787
  this.debuglog('get_blackout_games calculating blackout expiry')
3690
3788
  let date_cache_data = await this.getDayData(gameDate)
3691
3789
  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 ) {
3790
+ for (var j = 0; j < date_cache_data.dates[0].games.length; j++) {
3791
+ if ( game_pk == date_cache_data.dates[0].games[j].gamePk ) {
3694
3792
  this.debuglog('get_blackout_games found matching game')
3695
- let blackoutExpiry = await this.get_blackout_expiry(date_cache_data.dates[0].games[k])
3793
+ let blackoutExpiry = await this.get_blackout_expiry(date_cache_data.dates[0].games[j])
3696
3794
  this.debuglog('get_blackout_games calculated blackout expiry as ' + blackoutExpiry)
3697
3795
  blackouts[game_pk].blackoutExpiry = blackoutExpiry
3698
3796
  break
@@ -3706,6 +3804,45 @@ class sessionClass {
3706
3804
  return blackouts
3707
3805
  }
3708
3806
 
3807
+ // get all pre- and post-game for a date
3808
+ async get_pre_post_shows(gameDate='guide') {
3809
+ this.debuglog('get_pre_post_shows')
3810
+ let pregame_shows = {}
3811
+ let postgame_shows = {}
3812
+
3813
+ let cache_data
3814
+ cache_data = await this.getBlackoutsData(gameDate)
3815
+
3816
+ let teamTypes = ['home', 'away']
3817
+ let showTypes = ['preGame', 'postGame']
3818
+
3819
+ if ( cache_data && cache_data.results && (cache_data.results.length > 0) ) {
3820
+ for (var i = 0; i < cache_data.results.length; i++) {
3821
+ let game = cache_data.results[i]
3822
+ if ( game.prePostShows ) {
3823
+ for (var j = 0; j < teamTypes.length; j++) {
3824
+ let teamType = teamTypes[j]
3825
+ if ( game.prePostShows[teamType] ) {
3826
+ for (var k = 0; k < showTypes.length; k++) {
3827
+ let showType = showTypes[k]
3828
+ if ( game.prePostShows[teamType][showType] && game.prePostShows[teamType][showType].hasShow ) {
3829
+ this.debuglog('get_pre_post_shows found ' + teamType + ' ' + showType + ' ' + game.prePostShows[teamType].contentId)
3830
+ if ( game.prePostShows[teamType][showType].startTime ) {
3831
+ pregame_shows[game.prePostShows[teamType].contentId] = { team: teamType, start: game.prePostShows[teamType][showType].startTime }
3832
+ } else {
3833
+ postgame_shows[game.prePostShows[teamType].contentId] = { team: teamType }
3834
+ }
3835
+ }
3836
+ }
3837
+ }
3838
+ }
3839
+ }
3840
+ }
3841
+ }
3842
+
3843
+ return { pregame_shows, postgame_shows }
3844
+ }
3845
+
3709
3846
  async resetGameChanger(id, includeTeams, excludeTeams) {
3710
3847
  let today = this.liveDate()
3711
3848
  if ( !this.temp_cache.gamechanger || !this.temp_cache.gamechanger.date || (this.temp_cache.gamechanger.date != today) ) {
@@ -3835,7 +3972,12 @@ class sessionClass {
3835
3972
  }
3836
3973
 
3837
3974
  // 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) ) {
3975
+ if ( !cache_data.dates[0].games[i].broadcasts || (cache_data.dates[0].games[i].broadcasts.length == 0) ) {
3976
+ omitted_games.no_broadcast.push(teams)
3977
+ continue
3978
+ }
3979
+ let broadcast_count = await this.count_broadcasts(cache_data.dates[0].games[i].broadcasts, 'MLBTV')
3980
+ if ( broadcast_count == 0 ) {
3839
3981
  omitted_games.no_broadcast.push(teams)
3840
3982
  continue
3841
3983
  }
@@ -3852,8 +3994,8 @@ class sessionClass {
3852
3994
  continue
3853
3995
  }
3854
3996
 
3855
- // Game is blacked out
3856
- if ( this.temp_cache.gamechanger.blackouts[game_pk] ) {
3997
+ // All feeds are blacked out or not entitled
3998
+ 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
3999
  omitted_games.blackout.push(teams)
3858
4000
  continue
3859
4001
  }
@@ -4150,6 +4292,10 @@ class sessionClass {
4150
4292
  for (var y = 0; y < broadcasts.length; y++) {
4151
4293
  let broadcast = broadcasts[y]
4152
4294
  if ( (broadcast.availableForStreaming == true) && (broadcast.type == 'TV') && broadcast.mediaState && broadcast.mediaState.mediaStateCode && (broadcast.mediaState.mediaStateCode == 'MEDIA_ON') ) {
4295
+ // skip blackout feeds
4296
+ 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) ) {
4297
+ continue
4298
+ }
4153
4299
  // prefer fav team broadcasts
4154
4300
  if ( this.credentials.fav_teams.length > 0 ) {
4155
4301
  for (var z = 0; z < this.credentials.fav_teams.length; z++) {
@@ -4351,21 +4497,23 @@ class sessionClass {
4351
4497
  if ( subtitle ) {
4352
4498
  xml_output += ' <sub-title lang="en">' + subtitle + '</sub-title>' + "\n"
4353
4499
  }
4354
- xml_output += ' <desc lang="en">' + description.trim() + '</desc>' + "\n" +
4355
- ' <category lang="en">Sports</category>' + "\n" +
4356
- ' <category lang="en">Baseball</category>' + "\n" +
4357
- ' <category lang="en">Sports event</category>' + "\n" +
4358
- ' <icon src="' + icon + '"></icon>' + "\n"
4500
+ xml_output += ' <icon src="' + icon + '"></icon>' + "\n"
4501
+ if ( icon != OFF_AIR_LOGO ) {
4502
+ xml_output += ' <desc lang="en">' + description.trim() + '</desc>' + "\n" +
4503
+ ' <category lang="en">Sports</category>' + "\n" +
4504
+ ' <category lang="en">Baseball</category>' + "\n" +
4505
+ ' <category lang="en">Sports event</category>' + "\n" +
4506
+ ' <episode-num system="original-air-date">' + date + '</episode-num>' + "\n" +
4507
+ ' <new/>' + "\n" +
4508
+ ' <live/>' + "\n" +
4509
+ ' <sport>Baseball</sport>' + "\n"
4510
+ }
4359
4511
  if ( teamId ) {
4360
4512
  xml_output += ' <series-id system="team-id">' + teamId + '</series-id>' + "\n"
4361
4513
  }
4362
- xml_output += ' <episode-num system="original-air-date">' + date + '</episode-num>' + "\n"
4363
4514
  if ( gamePk ) {
4364
4515
  xml_output += ' <episode-num system="game-id">' + gamePk + '</episode-num>' + "\n"
4365
4516
  }
4366
- xml_output += ' <new/>' + "\n" +
4367
- ' <live/>' + "\n" +
4368
- ' <sport>Baseball</sport>' + "\n"
4369
4517
  if ( away_team ) {
4370
4518
  xml_output += ' <team lang="en">' + away_team + '</team>' + "\n"
4371
4519
  }
@@ -4388,7 +4536,47 @@ class sessionClass {
4388
4536
  channel_object.mediatype = channelMediaType
4389
4537
  return channel_object
4390
4538
  }
4391
-
4539
+
4540
+ async generate_off_air_event(offAir, channelid, gameDate, start, stop, title) {
4541
+ try {
4542
+ if ( offAir != 'false' ) {
4543
+ let today = this.liveDate()
4544
+ let nextWeek = new Date(today)
4545
+ nextWeek.setDate(nextWeek.getDate()+6)
4546
+ nextWeek = nextWeek.toISOString().substring(0,10)
4547
+ if ( !start ) {
4548
+ start = this.convertDateToXMLTV(new Date(today + ' 00:00:00'))
4549
+ }
4550
+ let offAirTitle = 'Off Air'
4551
+ let offAirSubtitle = ''
4552
+ let day = new Date(gameDate + ' 00:00:00').toLocaleString('en-US', { weekday: 'long' })
4553
+ if ( gameDate == today ) {
4554
+ day = 'Today'
4555
+ }
4556
+ if ( gameDate > nextWeek ) {
4557
+ day += ' ' + this.channelsFormattedDate(stop)
4558
+ }
4559
+ let time = new Date(stop).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })
4560
+ if ( offAir == 'channels' ) {
4561
+ offAirTitle = 'Upcoming: ' + time + ' ' + day + ', ' + title
4562
+ offAirSubtitle = ''
4563
+ } else {
4564
+ offAirSubtitle = 'next ' + day + ' ' + time
4565
+ }
4566
+ return await this.generate_xml_program(channelid, start, this.convertDateToXMLTV(new Date(stop)), offAirTitle, '', OFF_AIR_LOGO, '', offAirSubtitle)
4567
+ }
4568
+ } catch(e) {
4569
+ this.log('generate_off_air_event error : ' + e.message)
4570
+ }
4571
+ }
4572
+
4573
+ channelsFormattedTitle(subtitle, date) {
4574
+ return subtitle + ' *Live* ' + this.channelsFormattedDate(date)
4575
+ }
4576
+
4577
+ channelsFormattedDate(date) {
4578
+ return new Date(date).toLocaleString('en-us', { month: 'short', day: 'numeric' })
4579
+ }
4392
4580
  }
4393
4581
 
4394
4582
  module.exports = sessionClass