mlbserver 2023.4.5 → 2023.4.20

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 +1 -1
  2. package/index.js +106 -49
  3. package/package.json +1 -1
  4. package/session.js +91 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mlbserver
2
2
 
3
- Current version 2023.04.05
3
+ Current version 2023.04.20
4
4
 
5
5
  Credit to https://github.com/tonycpsu/streamglob and https://github.com/mafintosh/hls-decryptor
6
6
 
package/index.js CHANGED
@@ -37,7 +37,7 @@ const VALID_AUDIO_TRACKS = [ 'all', 'English', 'English Radio', 'Radio Española
37
37
  const DISPLAY_AUDIO_TRACKS = [ 'all', 'TV', 'Radio', 'Spanish', 'Alt.', 'Alt. Spanish', 'none' ]
38
38
  const ALTERNATE_AUDIO_TRACKS = [ VALID_AUDIO_TRACKS[4], VALID_AUDIO_TRACKS[5] ]
39
39
  const DEFAULT_MULTIVIEW_AUDIO_TRACK = 'English'
40
- const VALID_SKIP = [ 'off', 'breaks', 'idle time', 'pitches' ]
40
+ const VALID_SKIP = [ 'off', 'breaks', 'idle time', 'pitches', 'commercials' ]
41
41
  const VALID_PAD = [ 'off', 'on' ]
42
42
  const VALID_FORCE_VOD = [ 'off', 'on' ]
43
43
  const VALID_SCAN_MODES = [ 'off', 'on' ]
@@ -248,7 +248,7 @@ app.get('/stream.m3u8', async function(req, res) {
248
248
  let options = {}
249
249
  let includeBlackouts = 'false'
250
250
  let urlArray = req.url.split('?')
251
- if ( (urlArray.length == 1) || ((session.data.scan_mode == VALID_SCAN_MODES[1]) && req.query.team) || (!req.query.team && !req.query.src && !req.query.highlight_src && !req.query.event && !req.query.gamePk && !req.query.id && !req.query.mediaId && !req.query.contentId) ) {
251
+ if ( (urlArray.length == 1) || ((session.data.scan_mode == VALID_SCAN_MODES[1]) && req.query.team) || (!req.query.team && !req.query.src && !req.query.highlight_src && !req.query.eventURL && !req.query.event && !req.query.gamePk && !req.query.id && !req.query.mediaId && !req.query.contentId) ) {
252
252
  // load a sample encrypted HLS stream
253
253
  session.log('loading sample stream')
254
254
  options.resolution = VALID_RESOLUTIONS[0]
@@ -366,7 +366,17 @@ app.get('/stream.m3u8', async function(req, res) {
366
366
  options.contentId = contentId
367
367
 
368
368
  let skip_type = VALID_SKIP.indexOf(options.skip)
369
- await session.getSkipMarkers(contentId, skip_type, options.inning_number, options.inning_half)
369
+ // for commercial skip, just use the gdfp playlists and skip the ad inserts
370
+ if ( skip_type == 4 ) {
371
+ let new_streamURL = streamURL.replace('master_desktop_complete', 'master_desktop_complete_gdfp')
372
+ if ( new_streamURL == streamURL ) {
373
+ new_streamURL = streamURL.replace('master_desktop', 'master_desktop_gdfp')
374
+ }
375
+ session.debuglog('skipping commercials using gdfp playlist ' + new_streamURL)
376
+ streamURL = new_streamURL
377
+ } else {
378
+ await session.getSkipMarkers(contentId, skip_type, options.inning_number, options.inning_half)
379
+ }
370
380
  }
371
381
  }
372
382
 
@@ -660,6 +670,16 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
660
670
  return
661
671
  }
662
672
 
673
+ // Pass through any remaining caption tracks
674
+ if ( line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="') ) {
675
+ var parsed = line.match(',URI="([^"]+)"')
676
+ if ( parsed[1] ) {
677
+ newurl = '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim()))
678
+ return '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="' + newurl + '"'
679
+ }
680
+ return
681
+ }
682
+
663
683
  if (line[0] === '#') {
664
684
  return line
665
685
  }
@@ -755,7 +775,22 @@ app.get('/playlist', async function(req, res) {
755
775
  content_protect = '&content_protect=' + session.protection.content_protect
756
776
  }
757
777
 
758
- if ( (contentId) && ((inning_half != VALID_INNING_HALF[0]) || (inning_number != VALID_INNING_NUMBER[0]) || (skip != VALID_SKIP[0])) && (typeof session.temp_cache[contentId] !== 'undefined') && (typeof session.temp_cache[contentId].skip_markers !== 'undefined') ) {
778
+ // if skipping commercials, filter the playlist to remove ad insertion domains
779
+ if ( skip == 'commercials' ) {
780
+ session.debuglog('filtering commercial breaks')
781
+ let new_body = []
782
+ for (var i=0; i<body.length; i++) {
783
+ if ( body[i].includes('dai.google.com') ) {
784
+ new_body.pop()
785
+ if ( new_body[new_body.length-1] != '#EXT-X-DISCONTINUITY' ) {
786
+ new_body.push('#EXT-X-DISCONTINUITY')
787
+ }
788
+ } else {
789
+ new_body.push(body[i])
790
+ }
791
+ }
792
+ body = new_body
793
+ } else if ( (contentId) && ((inning_half != VALID_INNING_HALF[0]) || (inning_number != VALID_INNING_NUMBER[0]) || (skip != VALID_SKIP[0])) && (typeof session.temp_cache[contentId] !== 'undefined') && (typeof session.temp_cache[contentId].skip_markers !== 'undefined') ) {
759
794
  session.debuglog('pulling skip markers from temporary cache')
760
795
  skip_markers = session.temp_cache[contentId].skip_markers
761
796
  } else {
@@ -1238,10 +1273,12 @@ app.get('/', async function(req, res) {
1238
1273
  } else if ( level_ids == '1' ) {
1239
1274
  team_ids = session.getTeamIds()
1240
1275
  for (let i=0; i<session.credentials.fav_teams.length; i++) {
1241
- if ( level_ids == '1' ) {
1242
- level_ids = levels['All']
1276
+ if ( session.credentials.fav_teams[i] != '' ) {
1277
+ if ( level_ids == '1' ) {
1278
+ level_ids = levels['All']
1279
+ }
1280
+ team_ids += ',' + AFFILIATE_TEAM_IDS[session.credentials.fav_teams[i]]
1243
1281
  }
1244
- team_ids += ',' + AFFILIATE_TEAM_IDS[session.credentials.fav_teams[i]]
1245
1282
  }
1246
1283
  }
1247
1284
  let cache_name = gameDate
@@ -1470,7 +1507,21 @@ app.get('/', async function(req, res) {
1470
1507
 
1471
1508
  let blackouts = {}
1472
1509
 
1473
- if ( (mediaType == 'MLBTV') && level_ids.startsWith('1,') ) {
1510
+ if ( (mediaType == 'MLBTV') && ((level_ids == '1') || level_ids.startsWith('1,')) ) {
1511
+ // Recap Rundown beginning in 2023
1512
+ 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) ) {
1513
+ body += '<tr><td><span class="tooltip">VOD<span class="tooltiptext">Recap Rundown plays all of a day\'s recaps in order.</span></span></td><td>'
1514
+ let dateArray = gameDate.split('-')
1515
+ let querystring = '?event=recaprundown' + parseInt(dateArray[1]).toString() + '-' + parseInt(dateArray[2]).toString() + '-' + dateArray[0].substring(2,4)
1516
+ if ( linkType == VALID_LINK_TYPES[0] ) {
1517
+ if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1518
+ }
1519
+ if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1520
+ querystring += content_protect_b
1521
+ body += '<a href="' + thislink + querystring + '">Recap Rundown</a>'
1522
+ body += '</td></tr>' + "\n"
1523
+ }
1524
+
1474
1525
  if ( (gameDate >= today) && cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
1475
1526
  blackouts = await session.get_blackout_games(cache_data.dates[0].games, true)
1476
1527
  }
@@ -1487,7 +1538,7 @@ app.get('/', async function(req, res) {
1487
1538
  //big_inning = await session.generateBigInningSchedule(gameDate)
1488
1539
  }
1489
1540
  if ( big_inning && big_inning.start ) {
1490
- body += '<tr><td>' + new Date(big_inning.start).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + new Date(big_inning.end).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '</td><td>'
1541
+ body += '<tr><td><span class="tooltip">' + new Date(big_inning.start).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + new Date(big_inning.end).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '<span class="tooltiptext">Big Inning is the live look-in and highlights show. <a href="https://www.mlb.com/live-stream-games/big-inning">See here for more information</a>.</span></span></td><td>'
1491
1542
  let compareStart = new Date(big_inning.start)
1492
1543
  compareStart.setMinutes(compareStart.getMinutes()-10)
1493
1544
  let compareEnd = new Date(big_inning.end)
@@ -1514,7 +1565,7 @@ app.get('/', async function(req, res) {
1514
1565
  }
1515
1566
 
1516
1567
  // Game Changer
1517
- if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 1) ) {
1568
+ if ( (gameDate >= today) && cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 1) ) {
1518
1569
  let gameIndexes = await session.get_first_and_last_games(cache_data.dates[0].games, blackouts)
1519
1570
  if ( (typeof gameIndexes.firstGameIndex !== 'undefined') && (typeof gameIndexes.lastGameIndex !== 'undefined') && (gameIndexes.firstGameIndex !== gameIndexes.lastGameIndex) ) {
1520
1571
  let compareStart = new Date(cache_data.dates[0].games[gameIndexes.firstGameIndex].gameDate)
@@ -1527,8 +1578,8 @@ app.get('/', async function(req, res) {
1527
1578
  body += '<tr><td><span class="tooltip">' + compareStart.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + compareEnd.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '<span class="tooltiptext">The game changer stream will automatically switch between the highest leverage active live non-blackout games, and should be available whenever there are such games available. Does not support adaptive bitrate switching, will default to best resolution if not specified.</span></span></td><td>'
1528
1579
  if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1529
1580
  let streamURL = server + '/gamechanger.m3u8'
1581
+ let multiviewquerystring = streamURL + '?resolution=' + DEFAULT_MULTIVIEW_RESOLUTION + content_protect_b
1530
1582
  streamURL += content_protect_a
1531
- let multiviewquerystring = streamURL + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1532
1583
  if ( resolution != VALID_RESOLUTIONS[0] ) streamURL += '&resolution=' + resolution
1533
1584
  if ( linkType != VALID_LINK_TYPES[1] ) {
1534
1585
  streamURL = thislink + '?src=' + encodeURIComponent(streamURL) + '&startFrom=' + VALID_START_FROM[1] + content_protect_b
@@ -1713,49 +1764,55 @@ app.get('/', async function(req, res) {
1713
1764
  body += '><td>' + description + teams + pitchers + state + '</td>'
1714
1765
 
1715
1766
  // Check if Winter League / MiLB game first
1716
- if ( cache_data.dates[0].games[j].teams['home'].team.sport.id != '1' ) {
1767
+ if ( (cache_data.dates[0].games[j].teams['home'].team.sport.id != '1') && (mediaType == 'MLBTV') ) {
1717
1768
  body += "<td>"
1718
1769
  if ( cache_data.dates[0].games[j].broadcasts ) {
1770
+ let broadcastName = 'N/A'
1719
1771
  for (var k = 0; k < cache_data.dates[0].games[j].broadcasts.length; k++) {
1720
- if ( mediaType == 'MLBTV' ) {
1772
+ if ( cache_data.dates[0].games[j].broadcasts[k].name != 'Audio' ) {
1773
+ broadcastName = mediaType
1774
+ break
1775
+ }
1776
+ }
1777
+ if ( broadcastName == 'N/A' ) {
1778
+ body += broadcastName
1779
+ } else {
1780
+ // Check if game should be live
1781
+ if ( (cache_data.dates[0].games[j].status.detailedState != 'Postponed') && (cache_data.dates[0].games[j].status.detailedState != 'Cancelled') ) {
1721
1782
  // Check if game should be live
1722
- if ( (cache_data.dates[0].games[j].status.detailedState != 'Postponed') && (cache_data.dates[0].games[j].status.detailedState != 'Cancelled') ) {
1723
- // Check if game should be live
1724
- let currentTime = new Date()
1725
- let startTime = new Date(cache_data.dates[0].games[j].gameDate)
1726
- startTime.setMinutes(startTime.getMinutes()-30)
1727
- if ( (currentTime >= startTime) ) {
1728
- let gamePk = cache_data.dates[0].games[j].gamePk
1729
- let querystring
1730
- querystring = '?gamePk=' + gamePk
1731
- let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1732
- if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1733
- if ( linkType == VALID_LINK_TYPES[0] ) {
1734
- if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1735
- if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1736
- }
1737
- if ( resumeStatus == false ) {
1738
- if ( inning_half != VALID_INNING_HALF[0] ) querystring += '&inning_half=' + inning_half
1739
- if ( inning_number != VALID_INNING_NUMBER[0] ) querystring += '&inning_number=' + relative_inning
1740
- if ( skip != VALID_SKIP[0] ) querystring += '&skip=' + skip
1741
- //if ( skip_adjust != DEFAULT_SKIP_ADJUST ) querystring += '&skip_adjust=' + skip_adjust
1742
- }
1743
- if ( pad != VALID_PAD[0] ) querystring += '&pad=' + pad
1744
- if ( linkType == VALID_LINK_TYPES[1] ) {
1745
- let endTime = new Date(cache_data.dates[0].games[j].gameDate)
1746
- endTime.setHours(endTime.getHours()+4)
1747
- if ( currentTime < endTime ) {
1748
- if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1749
- }
1783
+ let currentTime = new Date()
1784
+ let startTime = new Date(cache_data.dates[0].games[j].gameDate)
1785
+ startTime.setMinutes(startTime.getMinutes()-30)
1786
+ if ( (currentTime >= startTime) ) {
1787
+ let gamePk = cache_data.dates[0].games[j].gamePk
1788
+ let querystring
1789
+ querystring = '?gamePk=' + gamePk
1790
+ let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1791
+ if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1792
+ if ( linkType == VALID_LINK_TYPES[0] ) {
1793
+ if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1794
+ if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1795
+ }
1796
+ if ( resumeStatus == false ) {
1797
+ if ( inning_half != VALID_INNING_HALF[0] ) querystring += '&inning_half=' + inning_half
1798
+ if ( inning_number != VALID_INNING_NUMBER[0] ) querystring += '&inning_number=' + relative_inning
1799
+ if ( skip != VALID_SKIP[0] ) querystring += '&skip=' + skip
1800
+ //if ( skip_adjust != DEFAULT_SKIP_ADJUST ) querystring += '&skip_adjust=' + skip_adjust
1801
+ }
1802
+ if ( pad != VALID_PAD[0] ) querystring += '&pad=' + pad
1803
+ if ( linkType == VALID_LINK_TYPES[1] ) {
1804
+ let endTime = new Date(cache_data.dates[0].games[j].gameDate)
1805
+ endTime.setHours(endTime.getHours()+4)
1806
+ if ( currentTime < endTime ) {
1807
+ if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1750
1808
  }
1751
- querystring += content_protect_b
1752
- multiviewquerystring += content_protect_b
1753
- body += '<a href="' + thislink + querystring + '">' + cache_data.dates[0].games[j].broadcasts[k].name + '</a>'
1754
- body += '<input type="checkbox" value="' + server + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1755
- } else {
1756
- body += cache_data.dates[0].games[j].broadcasts[k].name
1757
1809
  }
1758
- break
1810
+ querystring += content_protect_b
1811
+ multiviewquerystring += content_protect_b
1812
+ body += '<a href="' + thislink + querystring + '">' + broadcastName + '</a>'
1813
+ body += '<input type="checkbox" value="' + server + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1814
+ } else {
1815
+ body += broadcastName
1759
1816
  }
1760
1817
  }
1761
1818
  }
@@ -1961,7 +2018,7 @@ app.get('/', async function(req, res) {
1961
2018
  }
1962
2019
  body += '</p>' + "\n"
1963
2020
 
1964
- body += '<p><span class="tooltip">Skip<span class="tooltiptext">For video streams only (use the video "none" option above to apply it to audio streams): you can remove breaks, idle time, or non-action pitches from the stream (useful to make your own "condensed games").<br/><br/>NOTE: skip timings are only generated when the stream is loaded -- so for live games, it will only skip up to the time you loaded the stream.</span></span>: '
2021
+ body += '<p><span class="tooltip">Skip<span class="tooltiptext">For video streams only (use the video "none" option above to apply it to audio streams): you can remove all breaks, idle time, non-action pitches, or only commercial breaks from the stream (useful to make your own "condensed games").<br/><br/>NOTE: skip timings are only generated when the stream is loaded -- so for live games, it will only skip up to the time you loaded the stream. Also, commercial break skipping will ignore inning start options (it will always start from the beginning).</span></span>: '
1965
2022
  for (var i = 0; i < VALID_SKIP.length; i++) {
1966
2023
  body += '<button '
1967
2024
  if ( skip == VALID_SKIP[i] ) body += 'class="default" '
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2023.04.05",
3
+ "version": "2023.04.20",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -1225,6 +1225,17 @@ class sessionClass {
1225
1225
  }
1226
1226
  }
1227
1227
 
1228
+ setRecapRundownCacheExpiry(dateString, expiryDate) {
1229
+ if ( !this.cache.recapRundown ) {
1230
+ this.cache.recapRundown = {}
1231
+ }
1232
+ if ( !this.cache.recapRundown[dateString] ) {
1233
+ this.cache.recapRundown[dateString] = {}
1234
+ }
1235
+ this.cache.recapRundown[dateString].recapRundownCacheExpiry = expiryDate
1236
+ this.save_cache_data()
1237
+ }
1238
+
1228
1239
  cacheMediaId(contentId, mediaId, alternateAudioTracks) {
1229
1240
  this.createContentCache(contentId)
1230
1241
  this.cache.content[contentId].mediaId = mediaId
@@ -3211,6 +3222,79 @@ class sessionClass {
3211
3222
  }
3212
3223
  }
3213
3224
 
3225
+ // Get Recap Rundown data
3226
+ async getRecapRundownData(dateString) {
3227
+ try {
3228
+ this.debuglog('getRecapRundownData for ' + dateString)
3229
+
3230
+ let cache_data
3231
+ let cache_name = 'recaprundown' + dateString
3232
+ let cache_file = path.join(this.CACHE_DIRECTORY, cache_name + '.json')
3233
+ let currentDate = new Date()
3234
+ if ( !fs.existsSync(cache_file) || !this.cache || !this.cache.recapRundown || !this.cache.recapRundown[dateString] || !this.cache.recapRundown[dateString].recapRundownCacheExpiry || (currentDate > new Date(this.cache.recapRundown[dateString].recapRundownCacheExpiry)) ) {
3235
+ let reqObj = {
3236
+ url: 'https://dapi.mlbinfra.com/v2/content/en-us/videos/mlb-tv-recap-rundown-' + dateString,
3237
+ headers: {
3238
+ 'Authorization': 'Bearer ' + await this.getLoginToken() || this.halt('missing loginToken'),
3239
+ 'User-Agent': USER_AGENT,
3240
+ 'Origin': 'https://www.mlb.com',
3241
+ 'Referer': 'https://www.mlb.com',
3242
+ 'Content-Type': 'application/json',
3243
+ 'Accept-Encoding': 'gzip, deflate, br'
3244
+ },
3245
+ gzip: true
3246
+ }
3247
+ var response = await this.httpGet(reqObj, false)
3248
+ if ( response && this.isValidJson(response) ) {
3249
+ this.debuglog(response)
3250
+ cache_data = JSON.parse(response)
3251
+ this.save_json_cache_file(cache_name, cache_data)
3252
+
3253
+ // Default cache period is 5 minutes from now
3254
+ let fiveMinutesFromNow = new Date()
3255
+ fiveMinutesFromNow.setMinutes(fiveMinutesFromNow.getMinutes()+5)
3256
+ let cacheExpiry = fiveMinutesFromNow
3257
+
3258
+ // finally save the setting
3259
+ this.setRecapRundownCacheExpiry(dateString, cacheExpiry)
3260
+ this.save_cache_data()
3261
+ } else {
3262
+ this.log('error : invalid json from url ' + reqObj.url)
3263
+ return
3264
+ }
3265
+ } else {
3266
+ this.debuglog('using cached Recap Rundown data')
3267
+ cache_data = this.readFileToJson(cache_file)
3268
+ }
3269
+ if (cache_data) {
3270
+ return cache_data
3271
+ }
3272
+ } catch(e) {
3273
+ this.log('getRecapRundownData error : ' + e.message)
3274
+ }
3275
+ }
3276
+
3277
+ // Get Recap Rundown URL, used to determine the stream URL if available
3278
+ async getRecapRundownURL(dateString) {
3279
+ try {
3280
+ this.debuglog('getRecapRundownURL for ' + dateString)
3281
+
3282
+ let cache_data = await this.getRecapRundownData(dateString)
3283
+
3284
+ if ( cache_data && cache_data.fields && cache_data.fields.playbackScenarios && cache_data.fields.playbackScenarios && (cache_data.fields.playbackScenarios.length > 0) ) {
3285
+ this.debuglog('getRecapRundownURL found ' + cache_data.fields.playbackScenarios.length + ' playbackScenarios')
3286
+ for (var i=0; i<cache_data.fields.playbackScenarios.length; i++) {
3287
+ if ( cache_data.fields.playbackScenarios[i].playback && (cache_data.fields.playbackScenarios[i].playback == 'hlsCloud') && cache_data.fields.playbackScenarios[i].location ) {
3288
+ this.debuglog('found Recap Rundown url at ' + cache_data.fields.playbackScenarios[i].location)
3289
+ return cache_data.fields.playbackScenarios[i].location
3290
+ }
3291
+ }
3292
+ }
3293
+ } catch(e) {
3294
+ this.log('getRecapRundownURL error : ' + e.message)
3295
+ }
3296
+ }
3297
+
3214
3298
  // Get event stream URL
3215
3299
  async getEventStreamURL(eventName, gamePk=false) {
3216
3300
  if ( gamePk ) {
@@ -3225,7 +3309,13 @@ class sessionClass {
3225
3309
  if ( gamePk ) {
3226
3310
  playbackURL = 'https://dai.tv.milb.com/api/v2/playback-info/games/' + gamePk + '/contents/14862/products/milb-carousel'
3227
3311
  } else if ( eventName ) {
3228
- playbackURL = await this.getEventURL(eventName)
3312
+ if ( eventName.startsWith('RECAPRUNDOWN') ) {
3313
+ let dateString = eventName.substring(12)
3314
+ this.debuglog('getEventStreamURL RecapRundown for ' + dateString)
3315
+ playbackURL = await this.getRecapRundownURL(dateString)
3316
+ } else {
3317
+ playbackURL = await this.getEventURL(eventName)
3318
+ }
3229
3319
  }
3230
3320
  if ( !playbackURL ) {
3231
3321
  this.debuglog('no active event url')