mlbserver 2026.7.20 → 2026.7.22-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/index.js +1088 -396
  2. package/package.json +1 -1
  3. package/session.js +15 -0
  4. package/style.css +1119 -0
package/index.js CHANGED
@@ -251,6 +251,24 @@ function corsMiddleware(req, res, next) {
251
251
  httpAttach(multiview_app, corsMiddleware)
252
252
  multiview_app.listen(multiview_port)
253
253
 
254
+ // Listen for stylesheet requests
255
+ app.get('/style.css', async function (req, res) {
256
+ if (!(await protect(req, res))) return
257
+
258
+ session.requestlog('style.css', req, true)
259
+
260
+ var body = await session.getCSS()
261
+
262
+ if (!body) {
263
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
264
+ res.end('style.css not found')
265
+ return
266
+ }
267
+
268
+ res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8' })
269
+ res.end(body)
270
+ })
271
+
254
272
  // Listen for clear cache requests
255
273
  app.get('/clearcache', async function(req, res) {
256
274
  if ( ! (await protect(req, res)) ) return
@@ -1473,7 +1491,7 @@ app.get('/login', async function(req, res) {
1473
1491
  const redirect = req.query.redirect || '/';
1474
1492
  const error = req.query.error ? '<p style="color:red;">Invalid username or password</p>' : '';
1475
1493
  res.writeHead(200, {'Content-Type': 'text/html'});
1476
- res.end(`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Login - ${appname}</title><style>body{color:lightgray;background-color:black;font-family:Arial,Helvetica,sans-serif;}input{background-color:black;color:lightgray;border:1px solid lightgray;}button{background-color:black;color:lightgray;border:1px solid lightgray;}</style></head><body><h1>Login to ${appname}</h1>${error}<form method="POST" action="${http_root}/login"><input type="hidden" name="redirect" value="${redirect}"><p>Username: <input type="text" name="username" required></p><p>Password: <input type="password" name="password" required></p><p><button type="submit">Login</button></p></form></body></html>`);
1494
+ res.end(`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Login - ${appname}</title><style>html,body{background-color:#0e0e0e;color:LightGray;font-family:Verdana,Tahoma,Roboto,sans-serif}h1{margin:0;;color:goldenrod}button{color:#000;background-color:lightgray;min-width:3rem;border:none;padding:1px 5px 1px 5px;text-align:center;text-decoration:none;display:inline-block;font-size:.9rem;border-radius:4px;cursor:pointer;margin:2px}</style></head><body><h1>Login to ${appname}</h1>${error}<form method="POST" action="${http_root}/login"><input type="hidden" name="redirect" value="${redirect}"><p>Username: <input type="text" name="username" required></p><p>Password: <input type="password" name="password" required></p><p><button type="submit">Login</button></p></form></body></html>`);
1477
1495
  } catch (e) {
1478
1496
  session.log('login get request error : ' + e.message);
1479
1497
  res.end('login get request error, check log');
@@ -1644,6 +1662,7 @@ app.get('/', async function(req, res) {
1644
1662
  if ( req.query.mediaType ) {
1645
1663
  mediaType = req.query.mediaType
1646
1664
  }
1665
+ var mediaTypeTV = mediaType
1647
1666
  var resolution = VALID_RESOLUTIONS[0]
1648
1667
  if ( req.query.resolution ) {
1649
1668
  resolution = req.query.resolution
@@ -1696,15 +1715,9 @@ app.get('/', async function(req, res) {
1696
1715
  content_protect_b = '&content_protect=' + content_protect
1697
1716
  }
1698
1717
 
1699
- var body = '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta http-equiv="Content-type" content="text/html;charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><title>' + appname + '</title><link rel="icon" href="favicon.svg' + content_protect_a + '"><style type="text/css">input[type=text],input[type=button]{-webkit-appearance:none;-webkit-border-radius:0}body{width:480px;color:lightgray;background-color:black;font-family:Arial,Helvetica,sans-serif;-webkit-text-size-adjust:none}a{color:darkgray}button{color:lightgray;background-color:black}button.default{color:black;background-color:lightgray}table{width:100%;pad}table,th,td{border:1px solid darkgray;border-collapse:collapse}th,td{padding:5px}.tinytext,textarea,input[type="number"]{font-size:.8em}textarea{width:380px}.freegame,.freegame a{color:green}.blackout,.blackout a{text-decoration:line-through}'
1700
-
1701
- // Highlights CSS
1702
- body += '.modal{display:none;position:fixed;z-index:1;left:0;top:0;width:100%;height:100%;overflow:hidden;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}.modal-content{position:absolute;top:100px;bottom:20px;left:50%;transform:translateX(-50%);background-color:#fefefe;padding:10px;border:1px solid #888;width:360px;overflow-y:auto;color:black}#highlights{overflow-y:auto;}#highlights a{color:black}.close{color:black;float:right;font-size:28px;font-weight:bold;}#highlights a:hover,#highlights a:focus,.close:hover,.close:focus{color:gray;text-decoration:none;cursor:pointer;}'
1703
-
1704
- // Tooltip CSS
1705
- body += '.tooltip{position:relative;display:inline-block;border-bottom: 1px dotted gray;}.tooltip .tooltiptext{font-size:.8em;visibility:hidden;width:360px;background-color:gray;color:white;text-align:left;padding:5px;border-radius:6px;position:absolute;z-index:1;top:100%;left:75%;margin-left:-30px;}.tooltip:hover .tooltiptext{visibility:visible;}'
1718
+ var body = '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta http-equiv="Content-type" content="text/html;charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><title>' + appname + '</title><link rel="icon" href="favicon.svg' + content_protect_a + '"><link rel="stylesheet" href="/style.css' + content_protect_a + '">'
1706
1719
 
1707
- body += '</style><script type="text/javascript">' + "\n";
1720
+ body += '<script type="text/javascript">' + "\n";
1708
1721
 
1709
1722
  // Define option variables in page
1710
1723
  body += 'var date="' + gameDate + '";var level="' + level + '";var org="' + org + '";var mediaType="' + mediaType + '";var resolution="' + resolution + '";var audio_track="' + audio_track + '";var captions="' + captions + '";var force_vod="' + force_vod + '";var inning_half="' + inning_half + '";var inning_number="' + inning_number + '";var skip="' + skip + '";var skip_adjust="' + skip_adjust + '";var pad="' + pad + '";var linkType="' + linkType + '";var startFrom="' + startFrom + '";var scores="' + scores + '";var controls="' + controls + '";var scan_mode="' + scan_mode + '";var content_protect="' + content_protect + '";' + "\n"
@@ -1727,28 +1740,80 @@ app.get('/', async function(req, res) {
1727
1740
  body += '</script></head><body><h1>' + appname + '</h1>' + "\n"
1728
1741
 
1729
1742
  if (argv.login_page) {
1730
- body += '<p><a href="' + http_root + '/logout">Logout</a></p>' + "\n"
1743
+ body += '<p><form action="' + http_root + '/logout" method="GET"><button type="submit">Logout</button></form></p>' + "\n"
1731
1744
  }
1732
-
1733
- body += '<p><span class="tooltip tinytext">Touch or hover over an option name for more details</span></p>' + "\n"
1745
+ body +=`<div class="section">`
1746
+ body += '<div class="menuContainer settingsContainer">'
1747
+ body += '<div class="cardMenuHeader">Options</div>'
1748
+ body += '<div class="menuContent settingsContent">'
1734
1749
 
1735
1750
  todayUTCHours -= 4
1736
- body += '<p><span class="tooltip">Date<span class="tooltiptext">"today" lasts until ' + todayUTCHours + ' AM EST. Home page will default to yesterday between ' + todayUTCHours + ' AM - ' + (YESTERDAY_UTC_HOURS - 4) + ' AM EST.</span></span>: <input type="date" id="gameDate" value="' + gameDate + '"/> '
1751
+ const settingsInfoText = {
1752
+ dateInfo: '"today" lasts until ' + todayUTCHours + ' AM EST. Home page will default to yesterday between ' + todayUTCHours + ' AM - ' + (YESTERDAY_UTC_HOURS - 4) + ' AM EST.',
1753
+ levelInfo: 'Major or minor league level.',
1754
+ orgInfo: 'Major league parent organization.',
1755
+ mediaTypeInfo: 'Video is TV broadcasts, Audio is English radio, and Spanish is Spanish radio (not available for all games).',
1756
+ linkTypeInfo: 'Embed will play in your browser (with AirPlay support), Stream will give you a stream URL to open directly in media players like Kodi or VLC, Chromecast is a desktop browser-based casting site, Advanced will play in your desktop browser with some extra tools and debugging information (Advanced may require you to disable insecure / mixed content blocking in your browser), and Download will prompt your browser to save the stream to a TS (Transport Stream) file.<br><br>NOTE: Chromecast may not be able to resolve local domain names; if so, you can simply access this page (and thus the streams) using an IP address instead.',
1757
+ videoControlsInfo: 'Choose whether to show or hide controls on the embedded video page. Helpful to avoid timeline spoilers.',
1758
+ startFromInfo: 'For the embedded player only: Beginning will start playback at the beginning of the stream (may be 1 hour before game time for live games), and Live will start at the live point (if the event is live -- archive games should always start at the beginning). You can still seek anywhere.',
1759
+ inningInfo: 'For video streams only: choose the inning to start with (and the score to display, if applicable). Inning number is relative -- for example, selecting inning 7 here will show inning 7 for scheduled 9-inning games, but inning 5 for scheduled 7-inning games, for example. If an inning number is specified, seeking to an earlier point will not be possible. Default is the beginning of the stream. To use with radio, set the video track to "None".',
1760
+ scoresInfo: 'Choose whether to show scores on this web page. Combine this with the inning option to only show scores through the specified inning.',
1761
+ videoInfo: 'For video streams only: you can manually specifiy a video track (resolution) to use. Adaptive will let your client choose. Best will select either 1080p60 (MLB) or 720p60 (MiLB). 504p is default for multiview (see below).<br/><br/>None will allow to remove the video tracks, if you just want to listen to the audio while using the "start at inning" or "skip breaks" options enabled.',
1762
+ audioInfo: '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.',
1763
+ captionsInfo: 'For video streams only: you can disable the caption track, if one is present. This is handy if you do not want to disable it in your player each time.',
1764
+ skipInfo: '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/>NOTES: 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 skip will not work on pre-2024 games, or on MiLB games -- use skip breaks instead.',
1765
+ skipAdjustInfo: 'Seconds to adjust the skip time video segments, if necessary. Try a negative number if the plays are ending before the video segments begin; use a positive number if the video segments are ending before the play happens.',
1766
+ padInfo: 'You can pad archive streams with random extra time at the end, to help conceal timeline spoilers.',
1767
+ forceVodInfo: 'For streams only: if your client does not support seeking in mlbserver live streams, turning this on will make the stream look like a VOD stream instead, allowing the client to start at the beginning and allowing the user to seek within it. You will need to reload the stream to watch/view past the current time, though.'
1768
+ }
1769
+
1770
+ function settingInfoLabel(label, id) {
1771
+ return '<div class="settingLabel is-flex"><span>' + label + '</span><span class="info infoPadding" data-target="#' + id + '">?</span></div>'
1772
+ }
1773
+
1774
+ function settingInlineInfoLabel(label, id) {
1775
+ return '<span class="settingInlineLabel is-flex"><span>' + label + '</span><span class="info infoPadding" data-target="#' + id + '">?</span></span>'
1776
+ }
1777
+
1778
+ function settingInfo(id) {
1779
+ return '<div id="' + id + '" class="infoContainer"><div class="infoContent">' + settingsInfoText[id] + '</div></div>'
1780
+ }
1781
+
1782
+ body += '<div class="settingsGroup">'
1783
+ body += '<div class="settingRow">' + settingInfoLabel('Date', 'dateInfo') + '<div class="settingControl"><input type="date" id="gameDate" value="' + gameDate + '"/> '
1737
1784
  for (var i = 0; i < VALID_DATES.length; i++) {
1738
1785
  body += '<button '
1739
1786
  if ( ((VALID_DATES[i] == VALID_DATES[0]) && (gameDate == today)) || ((VALID_DATES[i] == VALID_DATES[1]) && (gameDate == yesterday)) ) body += 'class="default" '
1740
1787
  body += 'onclick="date=\'' + VALID_DATES[i] + '\';reload()">' + VALID_DATES[i] + '</button> '
1741
1788
  }
1742
- body += '</p>' + "\n" + '<p><span class="tinytext">Updated ' + session.getCacheUpdatedDate(cache_name) + '</span></p>' + "\n"
1789
+ body += '</div></div>' + "\n" + settingInfo('dateInfo') + '<div class="settingsMeta"><span>Updated ' + session.getCacheUpdatedDate(cache_name) + '</span></div>' + "\n"
1743
1790
 
1744
- body += '<p><span class="tooltip">Level<span class="tooltiptext">Major or minor league level</span></span>: '
1791
+ body += '<div class="settingRow">' + settingInfoLabel('Score', 'scoresInfo') + '<div class="settingControl">'
1792
+ for (var i = 0; i < VALID_SCORES.length; i++) {
1793
+ body += '<button '
1794
+ if ( scores == VALID_SCORES[i] ) body += 'class="default" '
1795
+ body += 'onclick="scores=\'' + VALID_SCORES[i] + '\';reload()">' + VALID_SCORES[i] + '</button> '
1796
+ }
1797
+ body += '</div></div>' + settingInfo('scoresInfo')
1798
+
1799
+ body += '<div class="settingRow">' + settingInfoLabel('Media Type', 'mediaTypeInfo') + '<div class="settingControl">'
1800
+ for (var i = 0; i < VALID_MEDIA_TYPES.length; i++) {
1801
+ body += '<button '
1802
+ if ( mediaType == VALID_MEDIA_TYPES[i] ) body += 'class="default" '
1803
+ body += 'onclick="mediaType=\'' + VALID_MEDIA_TYPES[i] + '\';reload()">' + VALID_MEDIA_TYPES[i] + '</button> '
1804
+ }
1805
+ body += '</div></div>' + "\n" + settingInfo('mediaTypeInfo')
1806
+
1807
+ body += '<details class="settingsDisclosure"><summary>Show more filters</summary><div class="settingsDisclosureContent">'
1808
+ body += '<div class="settingRow">' + settingInfoLabel('Level', 'levelInfo') + '<div class="settingControl">'
1745
1809
  for (const [key, value] of Object.entries(levels)) {
1746
1810
  body += '<button '
1747
1811
  if ( level == key ) body += 'class="default" '
1748
1812
  body += 'onclick="org=\'' + default_org + '\';level=\'' + key + '\';reload()">' + key + '</button> '
1749
1813
  }
1750
1814
 
1751
- body += ' or <span class="tooltip">Org<span class="tooltiptext">Major league parent organization</span></span>: '
1815
+ body += '<span class="settingsDivider">or</span>'
1816
+ body += settingInlineInfoLabel('Org', 'orgInfo') + ' '
1752
1817
  body += '<select id="org" onchange="level=\'' + default_org + '\';org=this.value;reload()">'
1753
1818
  body += '<option value="' + default_org + '">' + default_org + '</option>'
1754
1819
  var orgs = session.getOrgs()
@@ -1757,48 +1822,22 @@ app.get('/', async function(req, res) {
1757
1822
  if ( org == orgs[i] ) body += ' selected'
1758
1823
  body += '>' + orgs[i] + '</option> '
1759
1824
  }
1760
- body += '</select></p>' + "\n"
1825
+ body += '</select></div></div>' + settingInfo('levelInfo') + settingInfo('orgInfo') + '</div></details></div>' + "\n"
1761
1826
 
1762
- body += '<p><span class="tooltip">Media Type<span class="tooltiptext">Video is TV broadcasts, Audio is English radio, and Spanish is Spanish radio (not available for all games).</span></span>: '
1763
- for (var i = 0; i < VALID_MEDIA_TYPES.length; i++) {
1764
- body += '<button '
1765
- if ( mediaType == VALID_MEDIA_TYPES[i] ) body += 'class="default" '
1766
- body += 'onclick="mediaType=\'' + VALID_MEDIA_TYPES[i] + '\';reload()">' + VALID_MEDIA_TYPES[i] + '</button> '
1767
- }
1768
- body += '</p>' + "\n"
1769
-
1770
- body += '<p><span class="tooltip">Link Type<span class="tooltiptext">Embed will play in your browser (with AirPlay support), Stream will give you a stream URL to open directly in media players like Kodi or VLC, Chromecast is a desktop browser-based casting site, Advanced will play in your desktop browser with some extra tools and debugging information (Advanced may require you to disable insecure / mixed content blocking in your browser), and Download will prompt your browser to save the stream to a TS (Transport Stream) file.<br><br>NOTE: Chromecast may not be able to resolve local domain names; if so, you can simply access this page (and thus the streams) using an IP address instead.</span></span>: '
1771
- for (var i = 0; i < VALID_LINK_TYPES.length; i++) {
1772
- body += '<button '
1773
- if ( linkType == VALID_LINK_TYPES[i] ) body += 'class="default" '
1774
- body += 'onclick="linkType=\'' + VALID_LINK_TYPES[i] + '\';reload()">' + VALID_LINK_TYPES[i] + '</button> '
1775
- }
1776
- body += '</p>' + "\n"
1827
+ body += '<div class="settingsGroup">'
1777
1828
 
1778
- body += '<p>'
1779
1829
  if ( linkType == VALID_LINK_TYPES[0] ) {
1780
- body += '<span class="tooltip">Video Controls<span class="tooltiptext">Choose whether to show or hide controls on the embedded video page. Helpful to avoid timeline spoilers.</span></span>: '
1781
- for (var i = 0; i < VALID_CONTROLS.length; i++) {
1782
- body += '<button '
1783
- if ( controls == VALID_CONTROLS[i] ) body += 'class="default" '
1784
- body += 'onclick="controls=\'' + VALID_CONTROLS[i] + '\';reload()">' + VALID_CONTROLS[i] + '</button> '
1785
- }
1786
- body += '</p>' + "\n"
1787
-
1788
- body += '<p><span class="tooltip">Start From<span class="tooltiptext">For the embedded player only: Beginning will start playback at the beginning of the stream (may be 1 hour before game time for live games), and Live will start at the live point (if the event is live -- archive games should always start at the beginning). You can still seek anywhere.</span></span>: '
1830
+ body += '<div class="settingRow">' + settingInfoLabel('Start From', 'startFromInfo') + '<div class="settingControl">'
1789
1831
  for (var i = 0; i < VALID_START_FROM.length; i++) {
1790
1832
  body += '<button '
1791
1833
  if ( startFrom == VALID_START_FROM[i] ) body += 'class="default" '
1792
1834
  body += 'onclick="startFrom=\'' + VALID_START_FROM[i] + '\';reload()">' + VALID_START_FROM[i] + '</button> '
1793
1835
  }
1794
- body += "\n"
1795
- if ( mediaType == VALID_MEDIA_TYPES[0] ) {
1796
- body += 'or '
1797
- }
1836
+ body += '</div></div>' + settingInfo('startFromInfo')
1798
1837
  }
1799
1838
 
1800
1839
  if ( mediaType == VALID_MEDIA_TYPES[0] ) {
1801
- body += '<span class="tooltip">Inning<span class="tooltiptext">For video streams only: choose the inning to start with (and the score to display, if applicable). Inning number is relative -- for example, selecting inning 7 here will show inning 7 for scheduled 9-inning games, but inning 5 for scheduled 7-inning games, for example. If an inning number is specified, seeking to an earlier point will not be possible. Default is the beginning of the stream. To use with radio, set the video track to "None".</span></span>: '
1840
+ body += '<div class="settingRow">' + settingInfoLabel('Inning', 'inningInfo') + '<div class="settingControl">'
1802
1841
  body += '<select id="inning_half" onchange="inning_half=this.value;reload()">'
1803
1842
  for (var i = 0; i < VALID_INNING_HALF.length; i++) {
1804
1843
  body += '<option value="' + VALID_INNING_HALF[i] + '"'
@@ -1816,23 +1855,119 @@ app.get('/', async function(req, res) {
1816
1855
  }
1817
1856
  body += '</select>'
1818
1857
  }
1819
- body += '</p>' + "\n"
1858
+ if ( mediaType == VALID_MEDIA_TYPES[0] ) {
1859
+ body += '</div></div>' + "\n"
1860
+ body += settingInfo('inningInfo')
1861
+ }
1820
1862
 
1821
- body += '<p><span class="tooltip">Scores<span class="tooltiptext">Choose whether to show scores on this web page. Combine this with the inning option to only show scores through the specified inning.</span></span>: '
1822
- for (var i = 0; i < VALID_SCORES.length; i++) {
1863
+ body += '<details class="settingsDisclosure"><summary>Show more stream options</summary><div class="settingsDisclosureContent">'
1864
+
1865
+ body += '<div class="settingRow">' + settingInfoLabel('Link Type', 'linkTypeInfo') + '<div class="settingControl">'
1866
+ for (var i = 0; i < VALID_LINK_TYPES.length; i++) {
1823
1867
  body += '<button '
1824
- if ( scores == VALID_SCORES[i] ) body += 'class="default" '
1825
- body += 'onclick="scores=\'' + VALID_SCORES[i] + '\';reload()">' + VALID_SCORES[i] + '</button> '
1868
+ if ( linkType == VALID_LINK_TYPES[i] ) body += 'class="default" '
1869
+ body += 'onclick="linkType=\'' + VALID_LINK_TYPES[i] + '\';reload()">' + VALID_LINK_TYPES[i] + '</button> '
1870
+ }
1871
+ body += '</div></div>' + "\n" + settingInfo('linkTypeInfo')
1872
+
1873
+ if ( linkType == VALID_LINK_TYPES[0] ) {
1874
+ body += '<div class="settingRow">' + settingInfoLabel('Video Controls', 'videoControlsInfo') + '<div class="settingControl">'
1875
+ for (var i = 0; i < VALID_CONTROLS.length; i++) {
1876
+ body += '<button '
1877
+ if ( controls == VALID_CONTROLS[i] ) body += 'class="default" '
1878
+ body += 'onclick="controls=\'' + VALID_CONTROLS[i] + '\';reload()">' + VALID_CONTROLS[i] + '</button> '
1879
+ }
1880
+ body += '</div></div>' + "\n" + settingInfo('videoControlsInfo')
1881
+ }
1882
+
1883
+ if ( mediaType == VALID_MEDIA_TYPES[0] ) {
1884
+ body += '<div class="settingRow">' + settingInfoLabel('Video', 'videoInfo') + '<div class="settingControl">'
1885
+ body += '<button '
1886
+ if ( resolution == 'best' ) body += 'class="default" '
1887
+ body += 'onclick="resolution=\'best\';reload()">best</button> '
1888
+ for (var i = 0; i < VALID_RESOLUTIONS.length; i++) {
1889
+ body += '<button '
1890
+ if ( resolution == VALID_RESOLUTIONS[i] ) body += 'class="default" '
1891
+ body += 'onclick="resolution=\'' + VALID_RESOLUTIONS[i] + '\';reload()">' + VALID_RESOLUTIONS[i]
1892
+ if ( DISPLAY_BANDWIDTHS[i] != '' ) {
1893
+ body += '<br/><span>' + DISPLAY_BANDWIDTHS[i] + '</span>'
1894
+ }
1895
+ body += '</button> '
1896
+ }
1897
+ body += '</div></div>' + settingInfo('videoInfo')
1898
+
1899
+ body += '<div class="settingRow">' + settingInfoLabel('Audio', 'audioInfo') + '<div class="settingControl">'
1900
+ for (var i = 0; i < VALID_AUDIO_TRACKS.length; i++) {
1901
+ body += '<button '
1902
+ if ( audio_track == VALID_AUDIO_TRACKS[i] ) body += 'class="default" '
1903
+ body += 'onclick="audio_track=\'' + VALID_AUDIO_TRACKS[i] + '\';reload()">' + DISPLAY_AUDIO_TRACKS[i] + '</button> '
1904
+ }
1905
+ body += '</div></div>' + "\n" + settingInfo('audioInfo')
1906
+
1907
+ body += '<div class="settingRow">' + settingInfoLabel('Captions', 'captionsInfo') + '<div class="settingControl">'
1908
+ for (var i = 0; i < VALID_CAPTIONS.length; i++) {
1909
+ body += '<button '
1910
+ if ( captions == VALID_CAPTIONS[i] ) body += 'class="default" '
1911
+ body += 'onclick="captions=\'' + VALID_CAPTIONS[i] + '\';reload()">' + VALID_CAPTIONS[i] + '</button> '
1912
+ }
1913
+ body += '</div></div>' + settingInfo('captionsInfo')
1914
+
1915
+ body += '<div class="settingRow">' + settingInfoLabel('Skip', 'skipInfo') + '<div class="settingControl">'
1916
+ for (var i = 0; i < VALID_SKIP.length; i++) {
1917
+ body += '<button '
1918
+ if ( skip == VALID_SKIP[i] ) body += 'class="default" '
1919
+ body += 'onclick="skip=\'' + VALID_SKIP[i] + '\';reload()">' + VALID_SKIP[i] + '</button> '
1920
+ }
1921
+ if ( skip != VALID_SKIP[0] ) {
1922
+ body += settingInlineInfoLabel('Skip Adjust', 'skipAdjustInfo') + ' <input type="number" id="skip_adjust" value="' + skip_adjust + '" step="1" onchange="setTimeout(function(){skip_adjust=document.getElementById(\'skip_adjust\').value;reload()},750)" onblur="skip_adjust=this.value;reload()" style="vertical-align:top;font-size:.8em;width:3em"/>'
1923
+ }
1924
+ body += '</div></div>' + "\n" + settingInfo('skipInfo')
1925
+ if ( skip != VALID_SKIP[0] ) body += settingInfo('skipAdjustInfo')
1926
+ }
1927
+
1928
+ body += '<div class="settingRow">' + settingInfoLabel('Pad', 'padInfo') + '<div class="settingControl">'
1929
+ for (var i = 0; i < VALID_PAD.length; i++) {
1930
+ body += '<button '
1931
+ if ( pad == VALID_PAD[i] ) body += 'class="default" '
1932
+ body += 'onclick="pad=\'' + VALID_PAD[i] + '\';reload()">' + VALID_PAD[i] + '</button> '
1933
+ }
1934
+ body += '</div></div>' + "\n" + settingInfo('padInfo')
1935
+
1936
+ if ( (linkType == VALID_LINK_TYPES[1]) && (gameDate == today) ) {
1937
+ body += '<div class="settingRow">' + settingInfoLabel('VOD', 'forceVodInfo') + '<div class="settingControl">'
1938
+ for (var i = 0; i < VALID_FORCE_VOD.length; i++) {
1939
+ body += '<button '
1940
+ if ( force_vod == VALID_FORCE_VOD[i] ) body += 'class="default" '
1941
+ body += 'onclick="force_vod=\'' + VALID_FORCE_VOD[i] + '\';reload()">' + VALID_FORCE_VOD[i] + '</button> '
1942
+ }
1943
+ body += '<span>(if client does not support seeking in live streams)</span></div></div>' + "\n" + settingInfo('forceVodInfo')
1944
+ }
1945
+
1946
+ body += '</div></details></div></div></div></div>'
1947
+
1948
+ const subscriptionInfoText = {
1949
+ masn: 'MASN live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/MASN-In-Market-Offering">See here for more information</a>.',
1950
+ mlbn: 'MLB Network live stream is now available in the USA for paid MLBTV subscribers or as a paid add-on, in addition to authenticated TV subscribers. <a href="https://support.mlb.com/s/article/MLB-Network-Streaming-FAQ">See here for more information</a>.',
1951
+ snla: 'SNLA live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/SNLA-Plus-Subscription-Packages">See here for more information</a>.',
1952
+ sny: 'SNY live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/SNY-In-Market-Offering">See here for more information</a>.',
1953
+ bigInning: 'Big Inning is the live look-in and highlights show. <a href="https://support.mlb.com/s/article/What-Is-MLB-Big-Inning">See here for more information</a>.',
1954
+ gameChanger: '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 720p60 resolution if not specified.',
1955
+ streamFinder: 'The stream finder stream will automatically switch between games according to your uploaded preferences. This stream is not affiliated with Baseball Reference, do not contact them for support. Visit <a href="http://bit.ly/bbrefsf">http://bit.ly/bbrefsf</a> to create and export your preferences, then upload and save them to mlbserver <a href="#streamfinder">below</a>. Does not support adaptive bitrate switching, will default to 720p60 resolution if not specified.'
1826
1956
  }
1827
- body += '</p>' + "\n"
1828
1957
 
1829
- body += "<table>" + "\n"
1958
+ function subscriptionInfoLabel(label, id) {
1959
+ return '<span class="subscriptionLabel"><span>' + label + '</span><span class="info infoPadding" data-target="#' + id + '">?</span></span>'
1960
+ }
1961
+
1962
+ function subscriptionInfo(id, textKey) {
1963
+ return '<div id="' + id + '" class="infoContainer subscriptionInfoContainer"><div class="infoContent">' + subscriptionInfoText[textKey] + '</div></div>'
1964
+ }
1830
1965
 
1831
1966
  // Rename some parameters before display links
1832
1967
  var mediaFeedType = 'mediaFeedType'
1833
1968
  var language = 'en'
1834
1969
  if ( mediaType == VALID_MEDIA_TYPES[0] ) {
1835
- mediaType = 'MLBTV'
1970
+ mediaTypeTV = 'MLBTV'
1836
1971
  } else if ( mediaType == VALID_MEDIA_TYPES[2] ) {
1837
1972
  //mediaType = VALID_MEDIA_TYPES[1]
1838
1973
  language = 'es'
@@ -1858,10 +1993,107 @@ app.get('/', async function(req, res) {
1858
1993
 
1859
1994
  let entitlements = await session.getEntitlements()
1860
1995
 
1996
+ body += `<script>
1997
+ document.addEventListener("DOMContentLoaded", function () {
1998
+ var infoButtons = document.getElementsByClassName("info");
1999
+
2000
+ for (var i = 0; i < infoButtons.length; i++) {
2001
+ infoButtons[i].addEventListener("click", function () {
2002
+ var target = this.getAttribute("data-target");
2003
+
2004
+ if (!target) {
2005
+ return;
2006
+ }
2007
+
2008
+ var content = document.querySelector(target);
2009
+
2010
+ if (!content) {
2011
+ return;
2012
+ }
2013
+
2014
+ this.classList.toggle("active");
2015
+
2016
+ if (content.style.maxHeight) {
2017
+ content.style.maxHeight = null;
2018
+ } else {
2019
+ content.style.maxHeight = content.scrollHeight + "px";
2020
+ }
2021
+ });
2022
+ }
2023
+
2024
+ var closeButtons = document.getElementsByClassName("infoClose");
2025
+
2026
+ for (var j = 0; j < closeButtons.length; j++) {
2027
+ closeButtons[j].addEventListener("click", function () {
2028
+ var target = this.getAttribute("data-target");
2029
+
2030
+ if (!target) {
2031
+ return;
2032
+ }
2033
+
2034
+ var content = document.querySelector(target);
2035
+
2036
+ if (!content) {
2037
+ return;
2038
+ }
2039
+
2040
+ content.style.maxHeight = null;
2041
+
2042
+ var opener = document.querySelector('.info[data-target="' + target + '"]');
2043
+
2044
+ if (opener) {
2045
+ opener.classList.remove("active");
2046
+ }
2047
+ });
2048
+ }
2049
+ });
2050
+ </script>`
2051
+
2052
+
2053
+ let entitledMASN = (entitlements.includes('MASN_110'))
2054
+
2055
+ let entitledMLBN = (entitlements.includes('MLBN') || entitlements.includes('EXECMLB') || entitlements.includes('MLBTVMLBNADOBEPASS'))
2056
+
2057
+ let entitledSNLA = (entitlements.includes('SNLA_119'))
2058
+
2059
+ let entitledSNY = (entitlements.includes('SNY_121'))
2060
+
2061
+ let entitledBigInning = ( (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') )
2062
+
2063
+ let entitledChangerFinder = ((gameDate >= today) && cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 1))
2064
+
2065
+ let hasEntitledStreams = entitledMASN || entitledMLBN || entitledSNLA || entitledSNY || entitledBigInning || entitledChangerFinder
2066
+
2067
+ let subscriptionCardVis
2068
+ if (hasEntitledStreams) {subscriptionCardVis = ''} else {subscriptionCardVis ='is-invisible'}
2069
+
2070
+
2071
+ body += `<script>
2072
+ function toast() {
2073
+ var x = document.getElementById("snackbar");
2074
+ x.className = "show";
2075
+ setTimeout(function(){ x.className = x.className.replace("show", ""); }, 3000);
2076
+ }
2077
+ </script>`
2078
+ body +=`<div class="section">`
2079
+ body += `<div class="cardContainer ${subscriptionCardVis}">
2080
+ <div class="cardHeader flex-between">
2081
+ <div class="">
2082
+ <span class="time">Subscription streams</span>
2083
+ </div>
2084
+ </div>
2085
+ <div class="gameContent space-around">
2086
+ <div class="prePostLegend">
2087
+ <span><span class="prePostIndicator prePostLegendIndicator" title="Pregame show"><span class="prePostCircle prePostCircleActive"></span><span class="prePostCircle prePostCircleInactive"></span></span>Pregame show</span>
2088
+ <span><span class="prePostIndicator prePostLegendIndicator" title="Postgame show"><span class="prePostCircle prePostCircleInactive"></span><span class="prePostCircle prePostCircleActive"></span></span>Postgame show</span>
2089
+ </div>`
2090
+
2091
+
2092
+
1861
2093
  // MASN live stream for entitled subscribers
1862
2094
  try {
1863
- if ( entitlements.includes('MASN_110') ) {
1864
- body += '<tr><td><span class="tooltip">MASN<span class="tooltiptext">MASN live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/MASN-In-Market-Offering">See here for more information</a>.</span></span></td><td>'
2095
+ if ( entitledMASN ) {
2096
+ body += '<div class="flex-between subscriptionStream">' + subscriptionInfoLabel('MASN', 'masnInfo')
1865
2097
  let querystring = '?event=MASN'
1866
2098
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1867
2099
  if ( linkType == VALID_LINK_TYPES[0] ) {
@@ -1876,9 +2108,10 @@ app.get('/', async function(req, res) {
1876
2108
  }
1877
2109
  querystring += content_protect_b
1878
2110
  multiviewquerystring += content_protect_b
1879
- body += '<a href="' + thislink + querystring + '">MASN</a>'
1880
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1881
- body += '</td></tr>' + "\n"
2111
+ body += '<span><a href="' + thislink + querystring + '">MASN</a>'
2112
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)"></span>'
2113
+ body += '</div>' + "\n"
2114
+ body += subscriptionInfo('masnInfo', 'masn')
1882
2115
  } // end entitlements check
1883
2116
  } catch (e) {
1884
2117
  session.debuglog('MASN detect error : ' + e.message)
@@ -1886,8 +2119,8 @@ app.get('/', async function(req, res) {
1886
2119
 
1887
2120
  // MLB Network live stream for eligible USA subscribers
1888
2121
  try {
1889
- if ( entitlements.includes('MLBN') || entitlements.includes('EXECMLB') || entitlements.includes('MLBTVMLBNADOBEPASS') ) {
1890
- body += '<tr><td><span class="tooltip">MLB Network<span class="tooltiptext">MLB Network live stream is now available in the USA for paid MLBTV subscribers or as a paid add-on, in addition to authenticated TV subscribers. <a href="https://support.mlb.com/s/article/MLB-Network-Streaming-FAQ">See here for more information</a>.</span></span></td><td>'
2122
+ if ( entitledMLBN || entitlements.includes('EXECMLB') || entitlements.includes('MLBTVMLBNADOBEPASS') ) {
2123
+ body += '<div class="flex-between subscriptionStream">' + subscriptionInfoLabel('MLB Network', 'mlbnInfo')
1891
2124
  let querystring = '?event=MLBN'
1892
2125
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1893
2126
  if ( linkType == VALID_LINK_TYPES[0] ) {
@@ -1902,9 +2135,10 @@ app.get('/', async function(req, res) {
1902
2135
  }
1903
2136
  querystring += content_protect_b
1904
2137
  multiviewquerystring += content_protect_b
1905
- body += '<a href="' + thislink + querystring + '">MLB Network</a>'
1906
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1907
- body += '</td></tr>' + "\n"
2138
+ body += '<span><a href="' + thislink + querystring + '">MLB Network</a>'
2139
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)"></span>'
2140
+ body += '</div>' + "\n"
2141
+ body += subscriptionInfo('mlbnInfo', 'mlbn')
1908
2142
  } // end entitlements check
1909
2143
  } catch (e) {
1910
2144
  session.debuglog('MLB Network detect error : ' + e.message)
@@ -1912,8 +2146,8 @@ app.get('/', async function(req, res) {
1912
2146
 
1913
2147
  // SNLA live stream for entitled subscribers
1914
2148
  try {
1915
- if ( entitlements.includes('SNLA_119') ) {
1916
- body += '<tr><td><span class="tooltip">SportsNet LA<span class="tooltiptext">SNLA live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/SNLA-Plus-Subscription-Packages">See here for more information</a>.</span></span></td><td>'
2149
+ if ( entitledSNLA ) {
2150
+ body += '<div class="flex-between subscriptionStream">' + subscriptionInfoLabel('SportsNet LA', 'snlaInfo')
1917
2151
  let querystring = '?event=SNLA'
1918
2152
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1919
2153
  if ( linkType == VALID_LINK_TYPES[0] ) {
@@ -1928,9 +2162,10 @@ app.get('/', async function(req, res) {
1928
2162
  }
1929
2163
  querystring += content_protect_b
1930
2164
  multiviewquerystring += content_protect_b
1931
- body += '<a href="' + thislink + querystring + '">SNLA</a>'
1932
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1933
- body += '</td></tr>' + "\n"
2165
+ body += '<span><a href="' + thislink + querystring + '">SNLA</a>'
2166
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)"></span>'
2167
+ body += '</div>' + "\n"
2168
+ body += subscriptionInfo('snlaInfo', 'snla')
1934
2169
  } // end entitlements check
1935
2170
  } catch (e) {
1936
2171
  session.debuglog('SNLA detect error : ' + e.message)
@@ -1938,8 +2173,8 @@ app.get('/', async function(req, res) {
1938
2173
 
1939
2174
  // SNY live stream for entitled subscribers
1940
2175
  try {
1941
- if ( entitlements.includes('SNY_121') ) {
1942
- body += '<tr><td><span class="tooltip">SNY<span class="tooltiptext">SNY live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/SNY-In-Market-Offering">See here for more information</a>.</span></span></td><td>'
2176
+ if ( entitledSNY ) {
2177
+ body += '<div class="flex-between subscriptionStream">' + subscriptionInfoLabel('SNY', 'snyInfo')
1943
2178
  let querystring = '?event=SNY'
1944
2179
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1945
2180
  if ( linkType == VALID_LINK_TYPES[0] ) {
@@ -1954,9 +2189,10 @@ app.get('/', async function(req, res) {
1954
2189
  }
1955
2190
  querystring += content_protect_b
1956
2191
  multiviewquerystring += content_protect_b
1957
- body += '<a href="' + thislink + querystring + '">SNY</a>'
1958
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1959
- body += '</td></tr>' + "\n"
2192
+ body += '<span><a href="' + thislink + querystring + '">SNY</a>'
2193
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)"></span>'
2194
+ body += '</div>' + "\n"
2195
+ body += subscriptionInfo('snyInfo', 'sny')
1960
2196
  } // end entitlements check
1961
2197
  } catch (e) {
1962
2198
  session.debuglog('SNY detect error : ' + e.message)
@@ -1969,7 +2205,7 @@ app.get('/', async function(req, res) {
1969
2205
  }
1970
2206
  }
1971
2207
 
1972
- if ( (mediaType == 'MLBTV') && ((level_ids == levels['MLB']) || level_ids.startsWith(levels['MLB'] + ',')) ) {
2208
+ if ( (mediaTypeTV == 'MLBTV') && ((level_ids == levels['MLB']) || level_ids.startsWith(levels['MLB'] + ',')) ) {
1973
2209
  // Recap Rundown beginning in 2023, disabled because it stopped working
1974
2210
  /*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) ) {
1975
2211
  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>'
@@ -1986,14 +2222,16 @@ app.get('/', async function(req, res) {
1986
2222
 
1987
2223
  // Big Inning
1988
2224
  var big_inning
1989
- 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') ) {
2225
+ if ( entitledBigInning ) {
1990
2226
  // Scraped Big Inning schedule
1991
2227
  big_inning = await session.getBigInningSchedule(gameDate)
1992
2228
  }
1993
2229
  if ( big_inning ) {
2230
+ let bigInningRendered = false
1994
2231
  for (var i = 0; i < big_inning.length; i++) {
1995
- if ( big_inning[i].start ) {
1996
- body += '<tr><td><span class="tooltip">' + new Date(big_inning[i].start).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + new Date(big_inning[i].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://support.mlb.com/s/article/What-Is-MLB-Big-Inning">See here for more information</a>.</span></span></td><td>'
2232
+ if ( big_inning[i].start && !bigInningRendered ) {
2233
+ body += '<div class="flex-between subscriptionStream">' + "\n"
2234
+ body += '<span class="tinytext">' + new Date(big_inning[i].start).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + new Date(big_inning[i].end).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '</span>'
1997
2235
  let compareStart = new Date(big_inning[i].start)
1998
2236
  compareStart.setMinutes(compareStart.getMinutes()-10)
1999
2237
  let compareEnd = new Date(big_inning[i].end)
@@ -2013,18 +2251,20 @@ app.get('/', async function(req, res) {
2013
2251
  }
2014
2252
  querystring += content_protect_b
2015
2253
  multiviewquerystring += content_protect_b
2016
- body += '<a href="' + thislink + querystring + '">Big Inning</a>'
2017
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
2254
+ body += '<span>' + subscriptionInfoLabel('<a href="' + thislink + querystring + '">Big Inning</a>', 'bigInningInfo')
2255
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)"></span>'
2018
2256
  } else {
2019
- body += 'Big Inning'
2257
+ body += subscriptionInfoLabel('Big Inning', 'bigInningInfo')
2020
2258
  }
2021
- body += '</td></tr>' + "\n"
2259
+ body += '</div>' + "\n"
2260
+ body += subscriptionInfo('bigInningInfo', 'bigInning')
2261
+ bigInningRendered = true
2022
2262
  }
2023
2263
  }
2024
2264
  }
2025
-
2265
+
2026
2266
  // Game Changer and Stream Finder
2027
- if ( (gameDate >= today) && cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 1) ) {
2267
+ if ( entitledChangerFinder ) {
2028
2268
  let gameIndexes = await session.get_first_and_last_games(cache_data.dates[0].games, blackouts)
2029
2269
  if ( (typeof gameIndexes.firstGameIndex !== 'undefined') && (typeof gameIndexes.lastGameIndex !== 'undefined') && (gameIndexes.firstGameIndex !== gameIndexes.lastGameIndex) ) {
2030
2270
  let compareStart = new Date(cache_data.dates[0].games[gameIndexes.firstGameIndex].gameDate)
@@ -2034,7 +2274,8 @@ app.get('/', async function(req, res) {
2034
2274
  compareEnd.setHours(compareEnd.getHours()+4)
2035
2275
  }
2036
2276
  compareEnd.setHours(compareEnd.getHours()+4)
2037
- 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 720p60 resolution if not specified.</span></span></td><td>'
2277
+ body += '<div class="flex-between subscriptionStream">' + "\n"
2278
+ body += '<span class="tinytext">' + compareStart.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + compareEnd.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '</span>'
2038
2279
  if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
2039
2280
  let streamURL = server + '/gamechanger.m3u8'
2040
2281
  let multiviewquerystring = '/gamechanger.m3u8?resolution=' + DEFAULT_MULTIVIEW_RESOLUTION + content_protect_b
@@ -2050,14 +2291,15 @@ app.get('/', async function(req, res) {
2050
2291
  if ( linkType == VALID_LINK_TYPES[4] ) {
2051
2292
  streamURL += '&filename=' + gameDate + ' Game Changer'
2052
2293
  }
2053
- body += '<a href="' + streamURL + '">Game Changer</a>'
2054
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + multiviewquerystring + '" onclick="addmultiview(this, [], excludeTeams)">'
2294
+ body += '<span>' + subscriptionInfoLabel('<a href="' + streamURL + '">Game Changer</a>', 'gameChangerInfo')
2295
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + multiviewquerystring + '" onclick="addmultiview(this, [], excludeTeams)"></span>'
2055
2296
  } else {
2056
- body += 'Game Changer'
2297
+ body += subscriptionInfoLabel('Game Changer', 'gameChangerInfo')
2057
2298
  }
2058
- body += '</td></tr>' + "\n"
2059
-
2060
- 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 stream finder stream will automatically switch between games according to your uploaded preferences. This stream is not affiliated with Baseball Reference, do not contact them for support. Visit <a href="http://bit.ly/bbrefsf">http://bit.ly/bbrefsf</a> to create and export your preferences, then upload and save them to mlbserver <a href="#streamfinder">below</a>. Does not support adaptive bitrate switching, will default to 720p60 resolution if not specified.</span></span></td><td>'
2299
+ body += '</div>' + "\n"
2300
+ body += subscriptionInfo('gameChangerInfo', 'gameChanger')
2301
+ body += '<div class="flex-between subscriptionStream">' + "\n"
2302
+ body += '<span class="tinytext">' + compareStart.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + compareEnd.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '</span>'
2061
2303
  if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
2062
2304
  let streamURL = server + '/gamechanger.m3u8?streamFinder=on'
2063
2305
  let multiviewquerystring = '/gamechanger.m3u8?streamFinder=on&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION + content_protect_b
@@ -2073,15 +2315,17 @@ app.get('/', async function(req, res) {
2073
2315
  if ( linkType == VALID_LINK_TYPES[4] ) {
2074
2316
  streamURL += '&filename=' + gameDate + ' Stream Finder'
2075
2317
  }
2076
- body += '<a href="' + streamURL + '">Stream Finder</a>'
2077
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + multiviewquerystring + '" onclick="addmultiview(this, [], excludeTeams)">'
2318
+ body += '<span>' + subscriptionInfoLabel('<a href="' + streamURL + '">Stream Finder</a>', 'streamFinderInfo')
2319
+ body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + multiviewquerystring + '" onclick="addmultiview(this, [], excludeTeams)"></span>'
2078
2320
  } else {
2079
- body += 'Stream Finder'
2321
+ body += subscriptionInfoLabel('Stream Finder', 'streamFinderInfo')
2080
2322
  }
2081
- body += '</td></tr>' + "\n"
2323
+ body += '</div>'
2324
+ body += subscriptionInfo('streamFinderInfo', 'streamFinder')
2082
2325
  }
2083
2326
  }
2084
2327
  }
2328
+ body += `</div></div>`
2085
2329
 
2086
2330
  if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games ) {
2087
2331
  for (var j = 0; j < cache_data.dates[0].games.length; j++) {
@@ -2089,43 +2333,70 @@ app.get('/', async function(req, res) {
2089
2333
 
2090
2334
  let game_started = false
2091
2335
 
2092
- let awayteam = cache_data.dates[0].games[j].teams['away'].team.abbreviation
2093
- let awayteam_abbr
2336
+ let parentAwayVis = 'is-invisible'
2337
+ let awayteam = cache_data.dates[0].games[j].teams['away'].team.clubName
2338
+ let awayteam_abbr = cache_data.dates[0].games[j].teams['away'].team.abbreviation
2094
2339
  if ( cache_data.dates[0].games[j].teams['away'].team.sport.name != 'Major League Baseball' ) {
2095
2340
  awayteam = cache_data.dates[0].games[j].teams['away'].team.shortName
2096
2341
  let parentOrgName = cache_data.dates[0].games[j].teams['away'].team.parentOrgName
2097
- if (parentOrgName != 'Office of the Commissioner') awayteam += ' (' + session.getParent(parentOrgName) + ')'
2342
+ if (parentOrgName != 'Office of the Commissioner') parentAwayVis = '';
2098
2343
  awayteam_abbr = cache_data.dates[0].games[j].teams['away'].team.abbreviation
2099
2344
  awayteam_level = session.getLevelNameFromSportId(cache_data.dates[0].games[j].teams['away'].team.sport.id)
2100
2345
  }
2101
- let hometeam = cache_data.dates[0].games[j].teams['home'].team.abbreviation
2102
- let hometeam_abbr
2346
+ let parentHomeVis = 'is-invisible'
2347
+ let hometeam = cache_data.dates[0].games[j].teams['home'].team.clubName
2348
+ let hometeam_abbr = cache_data.dates[0].games[j].teams['home'].team.abbreviation
2103
2349
  if ( cache_data.dates[0].games[j].teams['home'].team.sport.name != 'Major League Baseball' ) {
2104
2350
  hometeam = cache_data.dates[0].games[j].teams['home'].team.shortName
2105
2351
  let parentOrgName = cache_data.dates[0].games[j].teams['home'].team.parentOrgName
2106
- if (parentOrgName != 'Office of the Commissioner') hometeam += ' (' + session.getParent(parentOrgName) + ')'
2352
+ if (parentOrgName != 'Office of the Commissioner') parentHomeVis = '';
2107
2353
  hometeam_abbr = cache_data.dates[0].games[j].teams['home'].team.abbreviation
2108
2354
  hometeam_level = session.getLevelNameFromSportId(cache_data.dates[0].games[j].teams['home'].team.sport.id)
2109
2355
  }
2110
2356
 
2111
- let teams = ""
2357
+ let awayDisplay = awayteam
2358
+ let homeDisplay = hometeam
2359
+ /*
2112
2360
  if ( awayteam_abbr ) {
2113
- teams += '<span class="tooltip">' + awayteam + '<span class="tooltiptext">Team Abbreviation: ' + awayteam_abbr + ', level ' + awayteam_level + '</span></span>'
2114
- } else {
2115
- teams += awayteam
2361
+ awayDisplay =
2362
+ '<span class="tooltip">' +
2363
+ awayteam +
2364
+ '<span class="tooltiptext left">' +
2365
+ awayteam_abbr + ' (' + awayteam_level + ')' +
2366
+ '</span></span>'
2116
2367
  }
2117
- teams += " @ "
2368
+
2118
2369
  if ( hometeam_abbr ) {
2119
- teams += '<span class="tooltip">' + hometeam + '<span class="tooltiptext">Team Abbreviation: ' + hometeam_abbr + ', level ' + hometeam_level + '</span></span>'
2120
- } else {
2121
- teams += hometeam
2370
+ homeDisplay =
2371
+ '<span class="tooltip">' +
2372
+ hometeam +
2373
+ '<span class="tooltiptext right">' +
2374
+ hometeam_abbr + ' (' + hometeam_level + ')' +
2375
+ '</span></span>'
2122
2376
  }
2123
- let filename_teams = awayteam + " @ " + hometeam
2124
- let pitchers = ""
2125
- let state = "<br/>"
2377
+ */
2378
+
2379
+ let filename_teams = awayteam + ' @ ' + hometeam
2380
+ let awayPitcher = ''
2381
+ let homePitcher = ''
2382
+ let state = ''
2383
+
2384
+ let awayscore = ''
2385
+ let homescore = ''
2386
+
2387
+ let noteworthyTag = ''
2388
+ let noteworthyTooltip = ''
2389
+ let noteworthyCSS = ''
2390
+ let tooltipCSS = ''
2391
+ let tooltipTextCSS = ''
2392
+
2393
+ let streamSource = {
2394
+ homeTV: '',
2395
+ awayTV: '',
2396
+ }
2126
2397
 
2127
2398
  if ( cache_data.dates[0].games[j].status.startTimeTBD == true ) {
2128
- state += "Time TBD"
2399
+ state = 'Time TBD'
2129
2400
  } else {
2130
2401
  let startTime = new Date(cache_data.dates[0].games[j].gameDate)
2131
2402
  state += startTime.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })
@@ -2139,8 +2410,8 @@ app.get('/', async function(req, res) {
2139
2410
  relative_inning = relative_inning < 0 ? 0 : relative_inning
2140
2411
  //if ( (scores == VALID_SCORES[1]) && (cache_data.dates[0].games[j].gameUtils.isLive || cache_data.dates[0].games[j].gameUtils.isFinal) && !cache_data.dates[0].games[j].gameUtils.isCancelled && !cache_data.dates[0].games[j].gameUtils.isPostponed ) {
2141
2412
  if ( (scores == VALID_SCORES[1]) && (abstractGameState != 'Preview') && (detailedState != 'Postponed') ) {
2142
- let awayscore = ''
2143
- let homescore = ''
2413
+ awayscore = ''
2414
+ homescore = ''
2144
2415
  if ( (inning_number != VALID_INNING_NUMBER[0]) && cache_data.dates[0].games[j].linescore && cache_data.dates[0].games[j].linescore.innings ) {
2145
2416
  awayscore = 0
2146
2417
  homescore = 0
@@ -2170,59 +2441,86 @@ app.get('/', async function(req, res) {
2170
2441
  }
2171
2442
  }
2172
2443
  if ( display_inning != '' ) {
2173
- state = "<br/>" + display_inning
2444
+ state = display_inning
2174
2445
  }
2175
2446
  } else {
2176
2447
  awayscore = cache_data.dates[0].games[j].teams['away'].score
2177
2448
  homescore = cache_data.dates[0].games[j].teams['home'].score
2178
- //if ( cache_data.dates[0].games[j].gameUtils.isLive && !cache_data.dates[0].games[j].gameUtils.isFinal ) {
2449
+ //if ( cache_data.dates[0].games[j].gameUtils.isLive && !cache_data.dates[0].games[j].gameUtils.isFinal ) {
2179
2450
  if ( abstractGameState == 'Live' ) {
2180
- state = "<br/>" + cache_data.dates[0].games[j].linescore.inningHalf.substr(0,1) + cache_data.dates[0].games[j].linescore.currentInning
2451
+ state = cache_data.dates[0].games[j].linescore.inningHalf.substr(0,1) + cache_data.dates[0].games[j].linescore.currentInning
2181
2452
  //} else if ( cache_data.dates[0].games[j].gameUtils.isFinal ) {
2182
2453
  } else if ( abstractGameState == 'Final' ) {
2183
- state = "<br/>" + detailedState
2454
+ state = detailedState
2184
2455
  }
2185
2456
  if ( cache_data.dates[0].games[j].flags ) {
2186
2457
  if ( cache_data.dates[0].games[j].flags.perfectGame == true ) {
2187
- state += "<br/>Perfect Game"
2458
+ noteworthyTag = 'PG'
2459
+ noteworthyTooltip = 'Perfect Game'
2460
+ noteworthyCSS = 'tag noteworthy'
2461
+ tooltipCSS = 'tooltip'
2462
+ tooltipTextCSS = 'tooltiptext'
2188
2463
  } else if ( cache_data.dates[0].games[j].flags.noHitter == true ) {
2189
- state += "<br/>No-Hitter"
2464
+ noteworthyTag = 'NH'
2465
+ noteworthyTooltip = 'No-Hitter'
2466
+ noteworthyCSS = 'tag noteworthy'
2467
+ tooltipCSS = 'tooltip'
2468
+ tooltipTextCSS = 'tooltiptext'
2469
+ } else {
2470
+ noteworthyTag = ''
2190
2471
  }
2191
2472
  }
2192
2473
  }
2193
- teams = awayteam + " " + awayscore + " @ " + hometeam + " " + homescore
2194
- //} else if ( cache_data.dates[0].games[j].gameUtils.isCancelled || cache_data.dates[0].games[j].gameUtils.isPostponed || cache_data.dates[0].games[j].gameUtils.isSuspended ) {
2474
+ //} else if ( cache_data.dates[0].games[j].gameUtils.isCancelled || cache_data.dates[0].games[j].gameUtils.isPostponed || cache_data.dates[0].games[j].gameUtils.isSuspended ) {
2195
2475
  } else if ( detailedState == 'Postponed' ) {
2196
- state = "<br/>" + detailedState
2197
- //} else if ( cache_data.dates[0].games[j].gameUtils.isDelayed ) {
2476
+ state = detailedState
2477
+ //} else if ( cache_data.dates[0].games[j].gameUtils.isDelayed ) {
2198
2478
  } else if ( detailedState.startsWith('Delayed') ) {
2199
- state += "<br/>" + detailedState
2479
+ state = detailedState
2200
2480
  }
2201
2481
 
2202
- var filename = gameDate + ' ' + filename_teams + ' '
2482
+ var filename = gameDate + ' ' + filename_teams + ' '
2483
+
2484
+ let scheduleVis = 'is-invisible'
2485
+ let doubleHeaderVis = 'is-invisible'
2486
+ let inningsVis = 'is-invisible'
2487
+ let blackoutVis = 'is-invisible'
2488
+ let blackoutToolTipVis = 'is-invisible'
2489
+ let blackoutType = ''
2490
+ let blackoutExpiry = ''
2491
+
2492
+ let scheduleDesc = ''
2493
+ let gameNo = ''
2494
+ let resumeText = ''
2495
+
2496
+ let highlightLink = ''
2497
+ let condensedLink = ''
2498
+ let recapLink = ''
2203
2499
 
2204
2500
  if ( cache_data.dates[0].games[j].doubleHeader != 'N' ) {
2205
- state += "<br/>Game " + cache_data.dates[0].games[j].gameNumber
2501
+ gameNo = cache_data.dates[0].games[j].gameNumber
2502
+ doubleHeaderVis = ''
2206
2503
  filename += 'Game ' + cache_data.dates[0].games[j].gameNumber + ' '
2207
2504
  }
2208
2505
  if ( cache_data.dates[0].games[j].description ) {
2209
- state += "<br/>" + cache_data.dates[0].games[j].description
2506
+ scheduleDesc = cache_data.dates[0].games[j].description
2507
+ scheduleVis = ''
2210
2508
  }
2211
2509
  if ( scheduledInnings != '9' ) {
2212
- state += "<br/>" + scheduledInnings + "-inning game"
2510
+ inningsVis = ''
2213
2511
  }
2214
2512
  var resumeStatus = false
2215
2513
  if ( cache_data.dates[0].games[j].resumeGameDate || cache_data.dates[0].games[j].resumedFromDate ) {
2216
- state += '<br/>Resum'
2514
+ let resumeText = ''
2217
2515
  let resumeDate
2218
2516
  if ( cache_data.dates[0].games[j].resumeGameDate ) {
2219
2517
  resumeDate = new Date(cache_data.dates[0].games[j].resumeDate)
2220
- state += 'ing on'
2518
+ resumeText += 'Resuming on'
2221
2519
  } else {
2222
2520
  resumeDate = new Date(cache_data.dates[0].games[j].resumedFrom)
2223
- state += 'ed from'
2521
+ resumeText += 'Resumed from'
2224
2522
  }
2225
- state += '<br/>' + resumeDate.toLocaleString('default', { month: 'long', day: 'numeric' })
2523
+ resumeText += '' + resumeDate.toLocaleString('default', { month: 'long', day: 'numeric' })
2226
2524
  // Also show the status by the media links, if one of them is live
2227
2525
  resumeStatus = 'archived'
2228
2526
  if ( (typeof cache_data.dates[0].games[j].broadcasts) != 'undefined' ) {
@@ -2236,61 +2534,82 @@ app.get('/', async function(req, res) {
2236
2534
  }
2237
2535
 
2238
2536
  if ( (cache_data.dates[0].games[j].teams['away'].probablePitcher && cache_data.dates[0].games[j].teams['away'].probablePitcher.fullName) || (cache_data.dates[0].games[j].teams['home'].probablePitcher && cache_data.dates[0].games[j].teams['home'].probablePitcher.fullName) ) {
2239
- pitchers = "<br/>"
2240
2537
  if ( cache_data.dates[0].games[j].teams['away'].probablePitcher && cache_data.dates[0].games[j].teams['away'].probablePitcher.fullName ) {
2241
2538
  if ( cache_data.dates[0].games[j].teams['away'].team.sport.name != 'Major League Baseball' ) {
2242
- pitchers += cache_data.dates[0].games[j].teams['away'].probablePitcher.fullName
2539
+ awayPitcher = getLastName(cache_data.dates[0].games[j].teams['away'].probablePitcher.fullName)
2243
2540
  } else {
2244
- pitchers += getLastName(cache_data.dates[0].games[j].teams['away'].probablePitcher.fullName)
2541
+ awayPitcher = getLastName(cache_data.dates[0].games[j].teams['away'].probablePitcher.fullName)
2245
2542
  }
2246
- pitchers += '</a>'
2247
2543
  } else {
2248
- pitchers += 'TBD'
2544
+ awayPitcher = 'TBD'
2249
2545
  }
2250
- pitchers += ' vs '
2251
2546
  if ( cache_data.dates[0].games[j].teams['home'].probablePitcher && cache_data.dates[0].games[j].teams['home'].probablePitcher.fullName ) {
2252
2547
  if ( cache_data.dates[0].games[j].teams['home'].team.sport.name != 'Major League Baseball' ) {
2253
- pitchers += cache_data.dates[0].games[j].teams['home'].probablePitcher.fullName
2548
+ homePitcher = getLastName(cache_data.dates[0].games[j].teams['home'].probablePitcher.fullName)
2254
2549
  } else {
2255
- pitchers += getLastName(cache_data.dates[0].games[j].teams['home'].probablePitcher.fullName)
2550
+ homePitcher = getLastName(cache_data.dates[0].games[j].teams['home'].probablePitcher.fullName)
2256
2551
  }
2257
- pitchers += '</a>'
2258
2552
  } else {
2259
- pitchers += 'TBD'
2553
+ homePitcher = 'TBD'
2260
2554
  }
2261
2555
  }
2262
2556
 
2263
- body += '<tr'
2557
+ function hexToRgba(hex, alpha) {
2558
+ const r = parseInt(hex.substring(0, 2), 16)
2559
+ const g = parseInt(hex.substring(2, 4), 16)
2560
+ const b = parseInt(hex.substring(4, 6), 16)
2561
+ return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')'
2562
+ }
2563
+
2264
2564
  let fav_style = ''
2265
- if ( argv.free && cache_data.dates[0].games[j].broadcasts && cache_data.dates[0].games[j].broadcasts[0] && cache_data.dates[0].games[j].broadcasts[0].freeGame ) {
2266
- body += ' class="freegame"'
2565
+ let freeGameVis = 'is-invisible'
2566
+ let favoritVis = 'is-invisible'
2567
+ let condensedCSS = 'videos shortButton'
2568
+ let recapCSS = 'videos shortButton'
2569
+ let highlightCSS = 'videos regularButton'
2570
+ let backgroundCSS = 'backgroundRegular'
2571
+
2572
+ if ( argv.free && cache_data.dates[0].games[j].broadcasts && cache_data.dates[0].games[j].broadcasts[0] && cache_data.dates[0].games[j].broadcasts[0].freeGame ) {
2573
+ freeGameVis = ''
2574
+
2267
2575
  } else if ( session.credentials.fav_teams.includes(cache_data.dates[0].games[j].teams['away'].team.abbreviation) || session.credentials.fav_teams.includes(cache_data.dates[0].games[j].teams['home'].team.abbreviation) ) {
2268
2576
  let fav_team = cache_data.dates[0].games[j].teams['away'].team.abbreviation
2269
2577
  if ( session.credentials.fav_teams.includes(cache_data.dates[0].games[j].teams['home'].team.abbreviation) ) {
2270
2578
  fav_team = cache_data.dates[0].games[j].teams['home'].team.abbreviation
2271
2579
  }
2272
- fav_style = ' style="color:#' + TEAM_COLORS[fav_team][0] + ';background:#' + TEAM_COLORS[fav_team][1] + ';"'
2273
- body += fav_style
2580
+
2581
+ favoritVis = ''
2582
+ let color1 = hexToRgba(TEAM_COLORS[fav_team][1], 0.2)
2583
+ let color2 = hexToRgba(TEAM_COLORS[fav_team][0], 0)
2584
+
2585
+ fav_style = ' style="background-image: linear-gradient(' + '0deg, ' + color1 + ', ' + color2 + '); border: 2px solid #' + TEAM_COLORS[fav_team][1] + '!important;; border-style: outset; border-radius: 3px;"'
2274
2586
  }
2275
- let description = ''
2587
+
2588
+ let gameLevel = session.getLevelNameFromSportId(cache_data.dates[0].games[j].teams['home'].team.sport.id);
2589
+ let gameLevelClass = 'level-' + gameLevel.toLowerCase().replace('+', '-plus')
2590
+
2276
2591
  if ( cache_data.dates[0].games[j].seriesDescription != 'Regular Season' ) {
2277
- description += cache_data.dates[0].games[j].seriesDescription + ': '
2592
+ gameLevel = cache_data.dates[0].games[j].seriesDescription
2278
2593
  }
2279
- body += '><td>' + description + teams + pitchers + state + '</td>'
2280
2594
 
2281
2595
  // Check if Winter League / MiLB game first
2282
- if ( (cache_data.dates[0].games[j].teams['away'].team.sport.id != levels['MLB']) && (cache_data.dates[0].games[j].teams['home'].team.sport.id != levels['MLB']) && (mediaType == 'MLBTV') ) {
2283
- body += "<td>"
2596
+ if ( (cache_data.dates[0].games[j].teams['away'].team.sport.id != levels['MLB']) && (cache_data.dates[0].games[j].teams['home'].team.sport.id != levels['MLB']) && (mediaTypeTV == 'MLBTV') ) {
2597
+ recapCSS = 'is-invisible'
2598
+ condensedCSS = 'is-invisible'
2599
+ highlightCSS = 'is-invisible'
2284
2600
  if ( cache_data.dates[0].games[j].broadcasts ) {
2285
2601
  let broadcastName = 'N/A'
2286
2602
  for (var k = 0; k < cache_data.dates[0].games[j].broadcasts.length; k++) {
2287
2603
  if ( cache_data.dates[0].games[j].broadcasts[k].name != 'Audio' ) {
2288
- broadcastName = mediaType
2604
+ broadcastName = mediaTypeTV
2289
2605
  break
2290
2606
  }
2291
2607
  }
2292
2608
  if ( broadcastName == 'N/A' ) {
2293
- body += broadcastName
2609
+ streamSource.homeTV +=
2610
+ '<span>' +
2611
+ '<span class="streamAction streamActionInactive">' + broadcastName + '</span>' +
2612
+ '</span>'
2294
2613
  } else {
2295
2614
  // Check if game should be live
2296
2615
  if ( (cache_data.dates[0].games[j].status.detailedState != 'Postponed') && (cache_data.dates[0].games[j].status.detailedState != 'Cancelled') ) {
@@ -2336,21 +2655,30 @@ app.get('/', async function(req, res) {
2336
2655
  }
2337
2656
  querystring += content_protect_b
2338
2657
  multiviewquerystring += content_protect_b
2339
- body += '<a href="' + thislink + querystring + '">' + broadcastName + '</a>'
2340
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
2658
+
2659
+ streamSource.homeTV +=
2660
+ '<span>' +
2661
+ '<input type="checkbox" value="http://127.0.0.1:' +
2662
+ session.data.port +
2663
+ '/stream.m3u8' +
2664
+ multiviewquerystring +
2665
+ '" onclick="addmultiview(this)">' +
2666
+ '<span>' +
2667
+ '<a class="streamAction" href="' + thislink + querystring + '">' + broadcastName + '</a>' +
2668
+ '</span>' +
2669
+ '</span>'
2341
2670
  } else {
2342
- body += broadcastName
2671
+ streamSource.homeTV +=
2672
+ '<span>' +
2673
+ '<span class="streamAction streamActionInactive">' + broadcastName + '</span>' +
2674
+ '</span>'
2343
2675
  }
2344
2676
  }
2345
2677
  }
2346
2678
  }
2347
- body += "</td>"
2348
2679
  } else {
2349
2680
  // Begin MLB games
2350
- if ( (typeof cache_data.dates[0].games[j].broadcasts) == 'undefined' ) {
2351
- body += "<td></td>"
2352
- } else {
2353
- body += "<td>"
2681
+ if ( (typeof cache_data.dates[0].games[j].broadcasts) != 'undefined' ) {
2354
2682
  for (var k = 0; k < cache_data.dates[0].games[j].broadcasts.length; k++) {
2355
2683
  let broadcast = cache_data.dates[0].games[j].broadcasts[k]
2356
2684
  if ( broadcast.availableForStreaming ) {
@@ -2360,47 +2688,43 @@ app.get('/', async function(req, res) {
2360
2688
  } else if ( broadcast.language == 'es' ) {
2361
2689
  mediaTitle = 'Spanish'
2362
2690
  }
2363
- if ( mediaTitle == mediaType ) {
2364
- // for video, check that it's not in-market
2365
- /*if ( (mediaType == 'MLBTV') && await session.check_in_market(cache_data.dates[0].games[j].content.media.epg[k].items[x]) ) {
2366
- continue
2367
- }*/
2691
+ if ( mediaTitle == mediaTypeTV ) {
2692
+ // for video, check that it's not in-market
2693
+ /*if ( (mediaTypeTV == 'MLBTV') && await session.check_in_market(cache_data.dates[0].games[j].content.media.epg[k].items[x]) ) {
2694
+ continue
2695
+ }*/
2368
2696
 
2369
2697
  // check if language is not set (video) or it matches requested language
2370
2698
  if ( broadcast.language == language ) {
2371
- let teamabbr
2372
-
2373
- if ( broadcast.isNational ) {
2374
- teamabbr = 'NATIONAL'
2375
- } else {
2376
- teamabbr = hometeam
2377
- if ( broadcast.homeAway == 'away' ) {
2378
- teamabbr = awayteam
2379
- }
2380
- }
2381
2699
  let station = broadcast.callSign
2382
-
2383
- if ( pre_post_shows.pregame_shows && pre_post_shows.pregame_shows[broadcast.mediaId] ) {
2384
- station = '/' + station
2385
- }
2386
- if ( pre_post_shows.postgame_shows && pre_post_shows.postgame_shows[broadcast.mediaId] ) {
2387
- station += '/'
2700
+ let hasPregameShow = pre_post_shows.pregame_shows && pre_post_shows.pregame_shows[broadcast.mediaId]
2701
+ let hasPostgameShow = pre_post_shows.postgame_shows && pre_post_shows.postgame_shows[broadcast.mediaId]
2702
+ let prePostIndicator = ''
2703
+
2704
+ if ( hasPregameShow || hasPostgameShow ) {
2705
+ let prePostTitle = 'Pregame show'
2706
+ if ( hasPregameShow && hasPostgameShow ) {
2707
+ prePostTitle = 'Pre- & postgame shows'
2708
+ } else if ( hasPostgameShow ) {
2709
+ prePostTitle = 'Postgame show'
2710
+ }
2711
+ prePostIndicator =
2712
+ '<span class="prePostIndicator" title="' + prePostTitle + '">' +
2713
+ '<span class="prePostCircle ' + (hasPregameShow ? 'prePostCircleActive' : 'prePostCircleInactive') + '"></span>' +
2714
+ '<span class="prePostCircle ' + (hasPostgameShow ? 'prePostCircleActive' : 'prePostCircleInactive') + '"></span>' +
2715
+ '</span>'
2388
2716
  }
2389
2717
 
2390
- // display blackout tooltip, if necessary
2391
2718
  if ( blackouts[gamePk] && blackouts[gamePk].blackout_feeds && blackouts[gamePk].blackout_feeds.includes(broadcast.mediaId) ) {
2392
- body += '<span class="tooltip"><span class="blackout">' + teamabbr + '</span><span class="tooltiptext">' + blackouts[gamePk].blackout_type
2393
- if ( blackouts[gamePk].blackout_type != 'Not entitled' ) {
2394
- body += ' video blackout until approx. 2.5 hours after the game'
2395
- if ( blackouts[gamePk].blackoutExpiry ) {
2396
- body += ' (~' + blackouts[gamePk].blackoutExpiry.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ')'
2397
- }
2719
+ blackoutVis = ''
2720
+ blackoutType = blackouts[gamePk].blackout_type
2721
+
2722
+ if ( blackoutType != 'Not entitled' ) {
2723
+ blackoutToolTipVis = ''
2724
+ blackoutExpiry = blackouts[gamePk].blackoutExpiry.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })
2725
+ // officially ~90 minutes, but more likely ~150 minutes or ~2.5 hours after the game ends
2398
2726
  }
2399
- body += '</span></span>'
2400
- } else {
2401
- body += teamabbr
2402
2727
  }
2403
- body += ': '
2404
2728
 
2405
2729
  if ( broadcast.mediaState && broadcast.mediaState.mediaStateCode && ((broadcast.mediaState.mediaStateCode == 'MEDIA_ON') || (broadcast.mediaState.mediaStateCode == 'MEDIA_ARCHIVE') || (abstractGameState == 'Final')) ) {
2406
2730
  let gameTime = new Date(cache_data.dates[0].games[j].gameDate)
@@ -2409,10 +2733,12 @@ app.get('/', async function(req, res) {
2409
2733
  game_started = true
2410
2734
  }
2411
2735
  let mediaId = broadcast.mediaId
2412
- if ( (mediaType == 'MLBTV') && (gameDate == today) && session.cache.media && session.cache.media[mediaId] && session.cache.media[mediaId].blackout && session.cache.media[mediaId].blackoutExpiry && (new Date(session.cache.media[mediaId].blackoutExpiry) > new Date()) ) {
2413
- body += '<span class="blackout">' + station + '</span>'
2736
+ if ( (mediaTypeTV == 'MLBTV') && (gameDate == today) && session.cache.media && session.cache.media[mediaId] && session.cache.media[mediaId].blackout && session.cache.media[mediaId].blackoutExpiry && (new Date(session.cache.media[mediaId].blackoutExpiry) > new Date()) ) {
2737
+ streamSource.awayTV +=
2738
+ '<span class="streamAction streamActionInactive blackoutstation">' + station + '</span>' +
2739
+ prePostIndicator
2414
2740
  } else {
2415
- let querystring
2741
+ let querystring
2416
2742
  querystring = '?mediaId=' + mediaId
2417
2743
 
2418
2744
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
@@ -2428,7 +2754,7 @@ app.get('/', async function(req, res) {
2428
2754
  if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
2429
2755
  if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
2430
2756
  }
2431
- if ( mediaType == 'MLBTV' ) {
2757
+ if ( mediaTypeTV == 'MLBTV' ) {
2432
2758
  if ( inning_half != VALID_INNING_HALF[0] ) querystring += '&inning_half=' + inning_half
2433
2759
  if ( inning_number != VALID_INNING_NUMBER[0] ) querystring += '&inning_number=' + relative_inning
2434
2760
  if ( skip != VALID_SKIP[0] ) querystring += '&skip=' + skip
@@ -2449,263 +2775,481 @@ app.get('/', async function(req, res) {
2449
2775
  }
2450
2776
  querystring += content_protect_b
2451
2777
  multiviewquerystring += content_protect_b
2452
- stationlink = '<a' + fav_style + ' href="' + thislink + querystring + '">' + station + '</a>'
2453
-
2454
- if ( blackouts[gamePk] && blackouts[gamePk].blackout_feeds && blackouts[gamePk].blackout_feeds.includes(broadcast.mediaId) ) {
2455
- body += '<span class="blackout">' + stationlink + '</span>'
2456
- } else {
2457
- body += stationlink
2458
- }
2459
- if ( mediaType == 'MLBTV' ) {
2460
- body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this, [\'' + awayteam + '\', \'' + hometeam + '\'])">'
2461
- }
2462
- if ( resumeStatus ) {
2463
- body += '('
2464
- // for suspended games that haven't finished yet, we can simply use the mediaState to determine the status
2465
- if ( resumeStatus == 'live' ) {
2466
- if ( broadcast.mediaState.mediaStateCode == 'MEDIA_ARCHIVE' ) {
2467
- body += '1'
2468
- } else {
2469
- body += '2'
2470
- }
2471
- // otherwise, for completed games, we need to check the airings data
2778
+ stationlink = '<a class="streamAction" href="' + thislink + querystring + '">' + station + '</a>'
2779
+
2780
+ let streamStationHtml = stationlink + prePostIndicator
2781
+ let resumeStreamSuffix = ''
2782
+ // For suspended games that are currently live, mark whether this feed is part 1 or part 2.
2783
+ if ( resumeStatus == 'live' ) {
2784
+ if ( broadcast.mediaState.mediaStateCode == 'MEDIA_ARCHIVE' ) {
2785
+ resumeStreamSuffix = '(1)'
2472
2786
  } else {
2473
- /*airings_data = await session.getAiringsData('', cache_data.dates[0].games[j].gamePk)
2474
- if ( airings_data.data && airings_data.data.Airings && (airings_data.data.Airings.length > 0) ) {
2475
- for (var y = 0; y < airings_data.data.Airings.length; y++) {
2476
- if ( airings_data.data.Airings[y].contentId == cache_data.dates[0].games[j].content.media.epg[k].items[x].contentId ) {
2477
- if ( (cache_data.dates[0].games[j].resumeDate && (cache_data.dates[0].games[j].resumeDate == airings_data.data.Airings[y].startDate)) || (cache_data.dates[0].games[j].resumedFrom && (cache_data.dates[0].games[j].gameDate == airings_data.data.Airings[y].startDate)) ) {
2478
- body += '2'
2479
- } else {
2480
- body += '1'
2481
- }
2482
- break
2483
- }
2484
- }
2485
- }*/
2787
+ resumeStreamSuffix = '(2)'
2486
2788
  }
2487
- body += ')'
2488
2789
  }
2790
+
2791
+ if (broadcast.homeAway == 'home') {
2792
+ streamSource.homeTV += '<span>'
2793
+
2794
+ if ( mediaTypeTV == 'MLBTV' ) {
2795
+ streamSource.homeTV +=
2796
+ '<input type="checkbox" value="http://127.0.0.1:' +
2797
+ session.data.port +
2798
+ '/stream.m3u8' +
2799
+ multiviewquerystring +
2800
+ '" onclick="addmultiview(this, [\'' +
2801
+ awayteam +
2802
+ '\', \'' +
2803
+ hometeam +
2804
+ '\'])">'
2805
+ }
2806
+
2807
+ streamSource.homeTV +=
2808
+ streamStationHtml +
2809
+ resumeStreamSuffix
2810
+
2811
+ streamSource.homeTV +=
2812
+ '</span>'
2813
+ } else if (broadcast.homeAway == 'away') {
2814
+ streamSource.awayTV += '<span>'
2815
+
2816
+ if ( mediaTypeTV == 'MLBTV' ) {
2817
+ streamSource.awayTV +=
2818
+ '<input type="checkbox" value="http://127.0.0.1:' +
2819
+ session.data.port +
2820
+ '/stream.m3u8' +
2821
+ multiviewquerystring +
2822
+ '" onclick="addmultiview(this, [\'' +
2823
+ awayteam +
2824
+ '\', \'' +
2825
+ hometeam +
2826
+ '\'])">'
2827
+ }
2828
+
2829
+ streamSource.awayTV +=
2830
+ streamStationHtml +
2831
+ resumeStreamSuffix
2832
+
2833
+ streamSource.awayTV +=
2834
+
2835
+ '</span>'
2836
+ }
2489
2837
  }
2490
2838
  // add YouTube link where available
2491
- /*if ( (mediaType == 'MLBTV') && cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube && cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube.videoId ) {
2839
+ /*if ( (mediaTypeTV == 'MLBTV') && cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube && cache_data.dates[0].games[j].content.media.epg[k].items[x].youtube.videoId ) {
2492
2840
  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>'
2493
2841
  }*/
2494
2842
  } else {
2843
+ let inactiveStation = station
2844
+
2495
2845
  if ( blackouts[gamePk] && blackouts[gamePk].blackout_feeds && blackouts[gamePk].blackout_feeds.includes(broadcast.mediaId) ) {
2496
- body += '<s>' + station + '</s>'
2846
+ inactiveStation = '<span class="streamAction streamActionInactive blackoutstation">' + station + '</span>' + prePostIndicator
2847
+ backgroundCSS = 'backgroundBlackout'
2497
2848
  } else {
2498
- body += station
2849
+ inactiveStation = '<span class="streamAction streamActionInactive">' + station + '</span>' + prePostIndicator
2499
2850
  }
2500
- } // end media active check
2501
- body += ', '
2502
- } // end streaming available check
2851
+
2852
+ if (broadcast.homeAway == 'home') {streamSource.homeTV +=
2853
+ '<div>' +
2854
+ '<div>' +
2855
+ inactiveStation +
2856
+ '</div>' +
2857
+ '</div>' } else if (broadcast.homeAway == 'away') {streamSource.awayTV +=
2858
+ '<div>' +
2859
+ '<div>' +
2860
+ inactiveStation +
2861
+ '</div>' +
2862
+ '</div>' }
2863
+ }
2864
+ }
2503
2865
  }
2504
2866
  }
2505
2867
  }
2506
- if ( body.substr(-2) == ', ' ) {
2507
- body = body.slice(0, -2)
2508
- }
2509
- //if ( (mediaType == 'MLBTV') && (game_started) && cache_data.dates[0].games[j].content && cache_data.dates[0].games[j].content.summary && cache_data.dates[0].games[j].content.summary.hasHighlightsVideo ) {
2510
- if ( (mediaType == 'MLBTV') && (game_started) ) {
2511
- body += '<br/><a' + fav_style + ' href="#" onclick="showhighlights(\'' + cache_data.dates[0].games[j].gamePk + '\',\'' + gameDate + '\'); return false;">Highlights</a>'
2512
- }
2513
- }
2514
- body += "</td>"
2515
- body += "</tr>" + "\n"
2516
- }
2517
- }
2518
- }
2519
- body += "</table>" + "\n"
2520
-
2521
- if ( (Object.keys(blackouts).length > 0) ) {
2522
- 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"
2523
- if ( (Object.keys(pre_post_shows).length > 0) ) {
2524
- body += '<br/>'
2525
- }
2526
- }
2527
-
2528
- 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)) ) {
2529
- 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"
2530
- if ( argv.free ) {
2531
- body += '<br/>'
2532
- }
2533
- }
2534
2868
 
2535
- if ( argv.free ) {
2536
- body += '<span class="freegame tooltip tinytext">green indicates a free game<span class="tooltiptext">Free games are available to anyone with an account, no subscription necessary. Blackouts still apply.</span></span>' + "\n"
2537
- }
2538
-
2539
- // Rename parameter back before displaying further links
2540
- if ( mediaType == 'MLBTV' ) {
2541
- mediaType = VALID_MEDIA_TYPES[0]
2542
- }
2869
+ if (mediaTypeTV == 'MLBTV' && game_started) {
2870
+ highlightLink +=
2871
+ '<a' +
2872
+ ' href="#" onclick="showhighlights(\'' +
2873
+ cache_data.dates[0].games[j].gamePk +
2874
+ '\',\'' +
2875
+ gameDate +
2876
+ '\'); return false;">Highlights</a>'
2877
+ } else { highlightLink += `Highlights`
2878
+ highlightCSS = 'videosInactive regularButton'
2879
+ }
2880
+
2881
+ if (mediaTypeTV == 'MLBTV' && abstractGameState == 'Final' && (detailedState != 'Postponed') ) {
2882
+ condensedLink +=
2883
+ '<a' +
2884
+ ' href="#" onclick="showcondensed(\'' +
2885
+ cache_data.dates[0].games[j].gamePk +
2886
+ '\',\'' +
2887
+ gameDate +
2888
+ '\'); return false;">Cond.</a>'
2889
+ } else { condensedLink += `Cond.`
2890
+ condensedCSS = 'videosInactive shortButton'}
2891
+
2892
+ if (mediaTypeTV == 'MLBTV' && abstractGameState == 'Final' && (detailedState != 'Postponed')) {
2893
+ recapLink +=
2894
+ '<a' +
2895
+ ' href="#" onclick="showrecap(\'' +
2896
+ cache_data.dates[0].games[j].gamePk +
2897
+ '\',\'' +
2898
+ gameDate +
2899
+ '\'); return false;">Recap</a>'
2900
+ } else {
2901
+ recapLink += `Recap`
2902
+ recapCSS = 'videosInactive shortButton'
2903
+ }
2543
2904
 
2544
- if ( mediaType == VALID_MEDIA_TYPES[0] ) {
2545
- body += '<p><span class="tooltip">Video<span class="tooltiptext">For video streams only: you can manually specifiy a video track (resolution) to use. Adaptive will let your client choose. Best will select either 1080p60 (MLB) or 720p60 (MiLB). 504p is default for multiview (see below).<br/><br/>None will allow to remove the video tracks, if you just want to listen to the audio while using the "start at inning" or "skip breaks" options enabled.</span></span>: '
2546
- body += '<button '
2547
- if ( resolution == 'best' ) body += 'class="default" '
2548
- body += 'onclick="resolution=\'best\';reload()">best</button> '
2549
- for (var i = 0; i < VALID_RESOLUTIONS.length; i++) {
2550
- body += '<button '
2551
- if ( resolution == VALID_RESOLUTIONS[i] ) body += 'class="default" '
2552
- body += 'onclick="resolution=\'' + VALID_RESOLUTIONS[i] + '\';reload()">' + VALID_RESOLUTIONS[i]
2553
- if ( DISPLAY_BANDWIDTHS[i] != '' ) {
2554
- body += '<br/><span class="tinytext">' + DISPLAY_BANDWIDTHS[i] + '</span>'
2555
2905
  }
2556
- body += '</button> '
2557
- }
2558
- body += '</p>' + "\n"
2559
-
2560
- 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>: '
2561
- for (var i = 0; i < VALID_AUDIO_TRACKS.length; i++) {
2562
- body += '<button '
2563
- if ( audio_track == VALID_AUDIO_TRACKS[i] ) body += 'class="default" '
2564
- body += 'onclick="audio_track=\'' + VALID_AUDIO_TRACKS[i] + '\';reload()">' + DISPLAY_AUDIO_TRACKS[i] + '</button> '
2565
- }
2566
- body += '</p>' + "\n"
2567
-
2568
- body += '<p><span class="tooltip">Captions<span class="tooltiptext">For video streams only: you can disable the caption track, if one is present. This is handy if you do not want to disable it in your player each time.</span></span>: '
2569
- for (var i = 0; i < VALID_CAPTIONS.length; i++) {
2570
- body += '<button '
2571
- if ( captions == VALID_CAPTIONS[i] ) body += 'class="default" '
2572
- body += 'onclick="captions=\'' + VALID_CAPTIONS[i] + '\';reload()">' + VALID_CAPTIONS[i] + '</button> '
2573
2906
  }
2574
- body += '</p>' + "\n"
2907
+ let pitcherVis = 'is-invisible'
2908
+ if (awayPitcher || homePitcher) { pitcherVis ='' }
2909
+ body +=`
2910
+ <div class="cardContainer ${backgroundCSS} ${gameLevelClass}" ${fav_style}>
2911
+
2912
+ <div class="cardHeader flex-between">
2913
+ <div class="flex-between">
2914
+ <div class="tag level ${gameLevelClass}">${gameLevel}</div>
2915
+
2916
+ </div>
2917
+ <div class="cardTags">
2918
+ <span class="tooltip ${scheduleVis}">
2919
+ <span class="tag schedule">SCD
2920
+ <span class="tooltiptext"> ${scheduleDesc}<br>${resumeText}</span>
2921
+ </span>
2922
+ </span>
2923
+ <span class="tooltip ${doubleHeaderVis}">
2924
+ <span class="tag gameno">G${gameNo}
2925
+ <span class="tooltiptext"> Game ${gameNo}</span>
2926
+ </span>
2927
+ </span>
2928
+ <span class="tooltip ${inningsVis}">
2929
+ <span class="tag innings">${scheduledInnings}IN
2930
+ <span class="tooltiptext">${scheduledInnings}-inning game </span>
2931
+ </span>
2932
+ </span>
2933
+ <span class="tooltip ${blackoutVis}">
2934
+ <span class="tag blackout">BLACKOUT
2935
+ <span class="tooltiptext">${blackoutType}
2936
+ <span class="${blackoutToolTipVis}"> video blackout until approx. ${blackoutExpiry}</span>
2937
+ </span>
2938
+ </span>
2939
+ </span>
2940
+ <span class="tooltip ${freeGameVis}">
2941
+ <span class="tag freegame">Free
2942
+ <span class="tooltiptext">Free Game</span>
2943
+ </span>
2944
+ </span>
2945
+ <span class="tooltip ${favoritVis}">
2946
+ <span class="tag favorite">FAV
2947
+ <span class="tooltiptext">Favorite team</span>
2948
+ </span>
2949
+ </span>
2950
+ <span class="${tooltipCSS}">
2951
+ <span class="${noteworthyCSS}">${noteworthyTag}
2952
+ <span class="${tooltipTextCSS}">${noteworthyTooltip}</span>
2953
+ </span>
2954
+ </span>
2955
+ <span class="time">${state}</span>
2956
+ </div>
2957
+ </div>
2958
+
2959
+ <div class="gameContent">
2960
+
2961
+ <div class="row ${gameLevel}">
2962
+ <div class="team">
2963
+ <span class="pitcher">${awayPitcher}</span>
2964
+ <span class="teamName">${awayteam}</span>
2965
+
2966
+
2967
+ </div>
2968
+ <div class="score">
2969
+ <span class="">${awayscore}</span>
2970
+ </div>
2971
+
2972
+ </div>
2973
+ <div class="parentOrg ${parentAwayVis}">` + cache_data.dates[0].games[j].teams['away'].team.parentOrgName + `</div>
2974
+
2975
+ <div class="row ${gameLevel}">
2976
+ <div class="team">
2977
+ <span class="pitcher">${homePitcher}</span>
2978
+ <span class="teamName">${hometeam}</span>
2979
+
2980
+
2981
+ </div>
2982
+ <div class="score">
2983
+ <span class="">${homescore}</span>
2984
+ </div>
2985
+ </div>
2986
+ <div class="parentOrg ${parentHomeVis}">` + cache_data.dates[0].games[j].teams['home'].team.parentOrgName + `</div>
2987
+
2988
+
2989
+
2990
+
2991
+ </div>
2992
+ <div class="streamContent">
2993
+ <span class="">${streamSource.awayTV}</span>
2994
+ <span class="flex-center ${highlightCSS}">★ ${highlightLink}</span>
2995
+ </div>
2996
+
2997
+ <div class="streamContent">
2998
+ <span class="">${streamSource.homeTV}</span>
2999
+ <div class="flex-center">
3000
+ <span class="flex-center ${recapCSS}">${recapLink}</span>
3001
+ <span class="flex-center ${condensedCSS}">${condensedLink}</span>
3002
+ </div>
3003
+ </div>
3004
+
2575
3005
 
2576
- 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/>NOTES: 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 skip will not work on pre-2024 games, or on MiLB games -- use skip breaks instead.</span></span>: '
2577
- for (var i = 0; i < VALID_SKIP.length; i++) {
2578
- body += '<button '
2579
- if ( skip == VALID_SKIP[i] ) body += 'class="default" '
2580
- body += 'onclick="skip=\'' + VALID_SKIP[i] + '\';reload()">' + VALID_SKIP[i] + '</button> '
2581
- }
2582
- if ( skip != VALID_SKIP[0] ) {
2583
- body += '<br><span class="tooltip">Skip Adjust<span class="tooltiptext">Seconds to adjust the skip time video segments, if necessary. Try a negative number if the plays are ending before the video segments begin; use a positive number if the video segments are ending before the play happens.</span></span>: <input type="number" id="skip_adjust" value="' + skip_adjust + '" step="1" onchange="setTimeout(function(){skip_adjust=document.getElementById(\'skip_adjust\').value;reload()},750)" onblur="skip_adjust=this.value;reload()" style="vertical-align:top;font-size:.8em;width:3em"/>'
2584
- }
2585
- body += '</p>' + "\n"
3006
+ </div>`
2586
3007
  }
3008
+ }
2587
3009
 
2588
- body += '<p><span class="tooltip">Pad<span class="tooltiptext">You can pad archive streams with random extra time at the end, to help conceal timeline spoilers.</span></span>: '
2589
- for (var i = 0; i < VALID_PAD.length; i++) {
2590
- body += '<button '
2591
- if ( pad == VALID_PAD[i] ) body += 'class="default" '
2592
- body += 'onclick="pad=\'' + VALID_PAD[i] + '\';reload()">' + VALID_PAD[i] + '</button> '
2593
- }
2594
- body += '</p>' + "\n"
3010
+ body +=`</div>`
2595
3011
 
2596
- if ( (linkType == VALID_LINK_TYPES[1]) && (gameDate == today) ) {
2597
- body += '<p><span class="tooltip">Force VOD<span class="tooltiptext">For streams only: if your client does not support seeking in mlbserver live streams, turning this on will make the stream look like a VOD stream instead, allowing the client to start at the beginning and allowing the user to seek within it. You will need to reload the stream to watch/view past the current time, though.</span></span>: '
2598
- for (var i = 0; i < VALID_FORCE_VOD.length; i++) {
2599
- body += '<button '
2600
- if ( force_vod == VALID_FORCE_VOD[i] ) body += 'class="default" '
2601
- body += 'onclick="force_vod=\'' + VALID_FORCE_VOD[i] + '\';reload()">' + VALID_FORCE_VOD[i] + '</button> '
2602
- }
2603
- body += '<span class="tinytext">(if client does not support seeking in live streams)</span></p>' + "\n"
2604
- }
3012
+ body +=`<div class="section">`
3013
+
2605
3014
 
2606
3015
  if ( mediaType == VALID_MEDIA_TYPES[0] ) {
2607
- body += '<table><tr><td><table><tr><td>1</td><td>2</tr><tr><td>3</td><td>4</td></tr></table><td><span class="tooltip">Multiview / Alternate Audio / Sync<span class="tooltiptext">For video streams only: create a new live stream combining 1-4 separate video streams, using the layout shown at left (if more than 1 video stream is selected). Check the boxes next to feeds above to add/remove them, then click "Start" when ready, "Stop" when done watching, or "Restart" to stop and start with the currently selected streams. May take up to 15 seconds after starting before it is ready to play.<br/><br/>No video scaling is performed: defaults to 540p video for each stream, which can combine to make one 1080p stream. Audio defaults to English (TV) audio. If you specify a different audio track instead, you can use the box after each URL below to adjust the sync in seconds (use positive values if audio is early and the audio stream needs to be padded with silence at the beginning to line up with the video; negative values if audio is late, and audio needs to be trimmed from the beginning.)<br/><br/>TIP #1: You can enter just 1 video stream here, at any resolution, to take advantage of the audio sync or alternate audio features without using multiview -- a single video stream will not be re-encoded and will be presented at its full resolution.<br/><br/>TIP #2: You can also manually enter streams from other sources like <a href="https://www.npmjs.com/package/milbserver" target="_blank">milbserver</a> in the boxes below. Make sure any manually entered streams have the desired resolution.<br/><br/>WARNING #1: if the mlbserver process dies or restarts while multiview is active, the ffmpeg encoding process will be orphaned and must be killed manually.<br/><br/>WARNING #2: If you did not specify a hardware encoder for ffmpeg on the command line, this will use your server CPU for encoding. Either way, your system may not be able to keep up with processing 4 video streams at once. Try fewer streams if you have perisistent trouble.</span></span>: <a id="startmultiview" href="" onclick="startmultiview(this);return false">Start'
3016
+ body += '<div class="menuContainer"><div class="cardMenuHeader">Multiview / Alternate Audio / Sync</div><div class="menuContent multiviewContent">' +
3017
+ '<div class="multiviewHeader"><div class="is-flex"><span>For video streams only: create a new live stream combining 1-4 separate video streams, using the layout shown at left (if more than 1 video stream is selected). Check the boxes next to feeds above to add/remove them, then click "Start" when ready, "Stop" when done watching, or "Restart" to stop and start with the currently selected streams.&nbsp;<span class="info infoPadding multiviewInfoButton" data-target="#multiviewInfo">?</span></span></div><div class="multiviewActions"><a id="startmultiview" class="multiviewAction multiviewActionPrimary" href="" onclick="startmultiview(this);return false">Start'
2608
3018
  if ( ffmpeg_status ) body += 'ed'
2609
- body += '</a> | <a id="stopmultiview" href="" onclick="stopmultiview(this);return false">Stop'
3019
+ body += '</a><a id="stopmultiview" class="multiviewAction" href="" onclick="stopmultiview(this);return false">Stop'
2610
3020
  if ( !ffmpeg_status ) body += 'ped'
2611
- body += '</a><br/>' + "\n"
2612
- body += '<span class="tinytext">(check boxes next to games to add, then click "Start";<br/>must click "Stop" link above when done, or manually kill ffmpeg)</span></td></tr><tr><td colspan="2">' + "\n"
3021
+ body += '</a></div></div>' + "\n" +
3022
+ '<div class="multiviewIntro"><div class="square-grid"><div>1</div><div>2</div><div>3</div><div>4</div></div>'
3023
+
3024
+ body += '<span>Check boxes next to games to add, then click Start. Click Stop when done, or manually kill ffmpeg.</span></div>' + ''
3025
+ body += `
3026
+ <div id="multiviewInfo" class="infoContainer">
3027
+ <div class="">
3028
+ <div class="infoContent">
3029
+ May take up to 15 seconds after starting before it is ready to play.<br/><br/>No video scaling is performed: defaults to 540p video for each stream, which can combine to make one 1080p stream. Audio defaults to English (TV) audio. If you specify a different audio track instead, you can use the box after each URL below to adjust the sync in seconds (use positive values if audio is early and the audio stream needs to be padded with silence at the beginning to line up with the video; negative values if audio is late, and audio needs to be trimmed from the beginning.)<br/><br/>TIP #1: You can enter just 1 video stream here, at any resolution, to take advantage of the audio sync or alternate audio features without using multiview -- a single video stream will not be re-encoded and will be presented at its full resolution.<br/><br/>TIP #2: You can also manually enter streams from other sources like <a href="https://www.npmjs.com/package/milbserver" target="_blank">milbserver</a> in the boxes below. Make sure any manually entered streams have the desired resolution.<br/><br/>WARNING #1: if the mlbserver process dies or restarts while multiview is active, the ffmpeg encoding process will be orphaned and must be killed manually.<br/><br/>WARNING #2: If you did not specify a hardware encoder for ffmpeg on the command line, this will use your server CPU for encoding. Either way, your system may not be able to keep up with processing 4 video streams at once. Try fewer streams if you have perisistent trouble.
3030
+ </div>
3031
+ </div>
3032
+
3033
+ </div>
3034
+ `
3035
+
2613
3036
  for (var i=1; i<=4; i++) {
2614
- body += i + ': <textarea id="multiview' + i + '" rows=2 cols=60 oninput="this.value=stream_substitution(this.value)"></textarea>'
2615
- body += '<input type="number" id="sync' + i + '" value="0.0" step=".1" style="vertical-align:top;font-size:.8em;width:3em"/>'
2616
- body += '<br/>' + "\n"
3037
+ body += '<div class="multiviewStreamRow"><label for="multiview' + i + '">' + i + ':</label><textarea id="multiview' + i + '" oninput="this.value=stream_substitution(this.value)"></textarea>'
3038
+ body += '<input type="number" id="sync' + i + '" value="0.0" step=".1"/></div>' + "\n"
2617
3039
  }
2618
- body += '<input type="checkbox" id="dvr"/> <span class="tooltip">DVR: allow pausing/seeking multiview<span class="tooltiptext">If this is enabled, it will use more disk space but you will be able to pause and seek in the multiview stream. Not necessary if you are strictly watching live.</span></span><br/>' + "\n"
2619
- body += '<input type="checkbox" id="faster" onchange="if (this.checked){document.getElementById(\'dvr\').checked=true}"/> <span class="tooltip">Encode faster than real-time<span class="tooltiptext">Implies DVR. Not necessary for live streams (which are only delivered in real-time), but if you want to seek ahead in archive streams using multiview, you may want to enable this. WARNING: ffmpeg may approach 100% CPU usage if you use this while combining multiple archive video streams in multiview.</span></span><br/>' + "\n"
2620
- body += '<input type="checkbox" id="reencode"/> <span class="tooltip">Re-encode all audio<span class="tooltiptext">Uses more CPU. Generally only necessary if you need the multiview stream to continue after one of the individual streams has ended. (Any streams with sync adjustments above will automatically be re-encoded, regardless of this setting.)</span></span><br/>' + "\n"
2621
- body += '<input type="checkbox" id="park_audio"/> <span class="tooltip">Park audio: filter out announcers<span class="tooltiptext">Implies re-encoding all audio. If this is enabled, an extra audio filter is applied to remove the announcer voices.</span></span><br/>' + "\n"
2622
- body += '<hr><span class="tooltip">Alternate audio URL and sync<span class="tooltiptext">Optional: you can also include a separate audio-only URL as an additional alternate audio track. Archive games will likely require a very large negative sync value, as the radio broadcasts may not be trimmed like the video archives.</span></span>:<br/><textarea id="audio_url" rows=2 cols=60 oninput="this.value=stream_substitution(this.value)"></textarea><input id="audio_url_seek" type="number" value="0" style="vertical-align:top;font-size:.8em;width:4em"/>'
2623
- body += '<hr>Watch: <a href="' + http_root + '/embed.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Embed</a> | <a href="' + http_root + '/stream.m3u8?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Stream</a> | <a href="' + http_root + '/chromecast.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Chromecast</a> | <a href="' + http_root + '/advanced.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Advanced</a> | <a href="' + http_root + '/download.ts?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '&filename=' + gameDate + ' Multiview">Download</a><br/><span class="tinytext">Kodi STRM files: <a href="' + http_root + '/kodi.strm?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Matrix/19+</a> (<a href="' + http_root + '/kodi.strm?version=18&src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Leia/18</a>)</span>'
2624
- body += '</td></tr></table><br/>' + "\n"
3040
+ body += `
3041
+ <div class="is-flex">
3042
+ <input type="checkbox" id="dvr"/>
3043
+ <span>DVR: allow pausing/seeking multiview</span>
3044
+ <div class="info infoPadding" data-target="#dvrInfo">?</div>
3045
+ </div>
3046
+ <div id="dvrInfo" class="infoContainer">
3047
+ <div class="infoContent">
3048
+ If this is enabled, it will use more disk space but you will be able to pause and seek in the multiview stream. Not necessary if you are strictly watching live.
3049
+ </div>
3050
+ </div>
3051
+
3052
+ <div class="is-flex">
3053
+ <input type="checkbox" id="faster" onchange="if (this.checked){document.getElementById('dvr').checked=true}"/>
3054
+ <span>Encode faster than real-time</span>
3055
+ <div class="info infoPadding" data-target="#fasterInfo">?</div>
3056
+ </div>
3057
+ <div id="fasterInfo" class="infoContainer">
3058
+ <div class="infoContent">
3059
+ Implies DVR. Not necessary for live streams (which are only delivered in real-time), but if you want to seek ahead in archive streams using multiview, you may want to enable this. WARNING: ffmpeg may approach 100% CPU usage if you use this while combining multiple archive video streams in multiview.
3060
+ </div>
3061
+ </div>
3062
+
3063
+ <div class="is-flex">
3064
+ <input type="checkbox" id="reencode"/>
3065
+ <span>Re-encode all audio</span>
3066
+ <div class="info infoPadding" data-target="#reencodeInfo">?</div>
3067
+ </div>
3068
+ <div id="reencodeInfo" class="infoContainer">
3069
+ <div class="infoContent">
3070
+ Uses more CPU. Generally only necessary if you need the multiview stream to continue after one of the individual streams has ended. Any streams with sync adjustments above will automatically be re-encoded, regardless of this setting.
3071
+ </div>
3072
+ </div>
3073
+
3074
+ <div class="is-flex">
3075
+ <input type="checkbox" id="park_audio"/>
3076
+ <span>Park audio: filter out announcers</span>
3077
+ <div class="info infoPadding" data-target="#parkAudioInfo">?</div>
3078
+ </div>
3079
+ <div id="parkAudioInfo" class="infoContainer">
3080
+ <div class="infoContent">
3081
+ Implies re-encoding all audio. If this is enabled, an extra audio filter is applied to remove the announcer voices.
3082
+ </div>
3083
+ </div>`
3084
+
3085
+ body += `
3086
+ <div class="is-flex">
3087
+ <span>Alternate audio URL and sync</span>
3088
+ <div class="info infoPadding" data-target="#altAudio">?</div>
3089
+ </div>
3090
+ <div id="altAudio" class="infoContainer">
3091
+ <div class="infoContent">
3092
+ Optional: you can also include a separate audio-only URL as an additional alternate audio track. Archive games will likely require a very large negative sync value, as the radio broadcasts may not be trimmed like the video archives.
3093
+ </div>
3094
+ </div>
3095
+ <div class="multiviewAudioRow">
3096
+ <textarea id="audio_url" oninput="this.value=stream_substitution(this.value)"></textarea>
3097
+ <input id="audio_url_seek" type="number" value="0"/>
3098
+ </div>
3099
+ <div class="multiviewWatchLinks">
3100
+ <a class="channelLink" href="${http_root}/embed.html?msrc=${encodeURIComponent(multiview_stream_url)}${content_protect_b}">Embed</a>
3101
+ <a class="channelLink" href="${http_root}/stream.m3u8?src=${encodeURIComponent(multiview_stream_url)}${content_protect_b}">Stream</a>
3102
+ <a class="channelLink" href="${http_root}/chromecast.html?msrc=${encodeURIComponent(multiview_stream_url)}${content_protect_b}">Chromecast</a>
3103
+ <a class="channelLink" href="${http_root}/advanced.html?msrc=${encodeURIComponent(multiview_stream_url)}${content_protect_b}">Advanced</a>
3104
+ <a class="channelLink" href="${http_root}/download.ts?src=${encodeURIComponent(multiview_stream_url)}${content_protect_b}&filename=${gameDate} Multiview">Download</a>
3105
+ </div>
3106
+ <div class="multiviewWatchLinks">
3107
+ <span class="channelNote">Kodi STRM files:</span>
3108
+ <a class="channelLink" href="${http_root}/kodi.strm?src=${encodeURIComponent(multiview_stream_url)}${content_protect_b}">Matrix/19+</a>
3109
+ <a class="channelLink" href="${http_root}/kodi.strm?version=18&src=${encodeURIComponent(multiview_stream_url)}${content_protect_b}">Leia/18</a>
3110
+ </div>
3111
+ </div></div>
3112
+ `
2625
3113
 
2626
- body += '<table><tr><td><p><a name="streamfinder"/><span class="tooltip">Stream Finder Settings<span class="tooltiptext">Automatically switches between games according to your preferences. This program is not affiliated with Baseball Reference, do not contact them for support.</span></span></p><p><a download="mlbserverStreamFinder.txt" href="' + http_root + '/downloadsettings' + content_protect_a + '">Click to Download Currently Stored Settings</a></p><p><b><u>Step 1</b></u><br/>Export and download your desired Stream Finder settings at this link:<br/><a href="https://www.baseball-reference.com/stream-finder.shtml" target="_blank">https://www.baseball-reference.com/stream-finder.shtml</a></p><form method="POST" enctype="multipart/form-data" action="' + http_root + '/upload' + content_protect_a + '"><p><b><u>Step 2</b></u><br/>Click this button and select the settings file you just downloaded:<br/><input name="file" type="file" onchange="form.submit()"/></p></form></td></tr></table><br/>' + "\n"
2627
- }
2628
-
2629
- body += '<table><tr><td>' + "\n"
2630
-
2631
- body += '<p><span class="tooltip">Live Channel Playlist, XMLTV Guide, ICS Calendar<span class="tooltiptext">Allows you to generate a M3U playlist of channels, and an XML file of guide listings for those channels, to import into TV/DVR/PVR software like Tvheadend or Jellyfin. You can also subscribe to the calendar links in your preferred calendar program/service to set up event notifications.<br/><br/>NOTE: May be helpful to specify a resolution above.</span></span>:</p>' + "\n"
2632
-
2633
- body += '<p><span class="tooltip">Scan Mode<span class="tooltiptext">During setup, some TV/DVR/PVR software will attempt to load all stream URLs. Turning Scan Mode ON will return a sample stream for all stream requests, thus satisfying that software without overloading mlbserver or excluding streams which aren\'t currently live. Once the channels are set up, turning Scan Mode OFF will restore normal stream behavior.<br/><br/>WARNING: Be sure your TV/DVR/PVR software doesn\'t periodically scan all channels automatically or you might overload mlbserver.</span></span>: '
3114
+ body += `
3115
+ <div class="menuContainer">
3116
+ <div class="cardMenuHeader">Stream Finder Settings</div>
3117
+ <div class="menuContent streamFinderContent">
3118
+ <div>Automatically switches between games according to your preferences. This program is not affiliated with Baseball Reference, do not contact them for support.</div>
3119
+ <div>
3120
+ <a class="channelLink" download="mlbserverStreamFinder.txt" href="${http_root}/downloadsettings${content_protect_a}">Download current settings</a>
3121
+ </div>
3122
+ <div class="streamFinderSteps">
3123
+ <div class="streamFinderStep">
3124
+ <div class="streamFinderStepNumber">1</div>
3125
+ <div class="streamFinderStepContent">
3126
+ <div class="streamFinderStepTitle">Export settings</div>
3127
+ <div class="streamFinderStepText">Create and export your Stream Finder settings at Baseball Reference.</div>
3128
+ <a class="channelLink" href="https://www.baseball-reference.com/stream-finder.shtml" target="_blank">Open Stream Finder</a>
3129
+ </div>
3130
+ </div>
3131
+ <div class="streamFinderStep">
3132
+ <div class="streamFinderStepNumber">2</div>
3133
+ <div class="streamFinderStepContent">
3134
+ <div class="streamFinderStepTitle">Upload settings</div>
3135
+ <div class="streamFinderStepText">Select the settings file you just downloaded.</div>
3136
+ <form method="POST" enctype="multipart/form-data" action="${http_root}/upload${content_protect_a}">
3137
+ <input name="file" type="file" onchange="form.submit()"/>
3138
+ </form>
3139
+ </div>
3140
+ </div>
3141
+ </div>
3142
+ </div>
3143
+ </div>` + "\n"
3144
+ }
3145
+ body +=`</div>`
3146
+ const channelInfoText = {
3147
+ scanMode: 'During setup, some TV/DVR/PVR software will attempt to load all stream URLs. Turning Scan Mode ON will return a sample stream for all stream requests, thus satisfying that software without overloading mlbserver or excluding streams which are not currently live. Once the channels are set up, turning Scan Mode OFF will restore normal stream behavior. WARNING: Be sure your TV/DVR/PVR software does not periodically scan all channels automatically or you might overload mlbserver.',
3148
+ allChannels: 'Will include all entitled live MLB broadcasts (games plus Big Inning, Game Changer, and Multiview, as well as MASN, 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.',
3149
+ byTeam: '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 broadcast if available. It will also include affiliate games for those organizations. Channels/games subject to blackout will be omitted by default.',
3150
+ byTeamRadio: 'Same as By team, but defaults to that team radio track, if available.',
3151
+ byTeamSpanish: 'Same as By team, but defaults to that team Spanish radio track, if available.',
3152
+ includeBlackouts: 'Adds an optional parameter to include channels/games subject to blackout, although you may not be able to play those games.',
3153
+ excludeTeam: 'Excluding a team (MLB only, by abbreviation, in a comma-separated list if more than 1) will exclude every game involving that team. Blackouts are already excluded without this parameter.',
3154
+ winterLeagues: 'Winter leagues include the Arizona Fall League, Dominican Winter League aka Liga de Beisbol Dominicano, and Mexican Winter League aka Liga Mexicana del Pacifico. Live stream only, does not support starting from the beginning or certain innings, skip options, etc.',
3155
+ masn: 'MASN live stream for entitled subscribers. See https://support.mlb.com/s/article/MASN-In-Market-Offering for more information.',
3156
+ mlbn: 'MLB Network live stream is now available in the USA for paid MLBTV subscribers or as a paid add-on, in addition to authenticated TV subscribers. See https://support.mlb.com/s/article/MLB-Network-Streaming-FAQ for more information.',
3157
+ snla: 'SNLA live stream for entitled subscribers. See https://support.mlb.com/s/article/SNLA-Plus-Subscription-Packages for more information.',
3158
+ sny: 'SNY live stream for entitled subscribers. See https://support.mlb.com/s/article/SNY-In-Market-Offering for more information.',
3159
+ bigInning: 'Big Inning is the live look-in and highlights show. See https://www.mlb.com/live-stream-games/big-inning for more information.',
3160
+ gameChanger: '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.',
3161
+ streamFinder: 'The stream finder stream will automatically switch between games according to your uploaded preferences. This stream is not affiliated with Baseball Reference, do not contact them for support. Visit http://bit.ly/bbrefsf to create and export your preferences, then upload and save them to mlbserver above. Does not support adaptive bitrate switching, will default to best resolution if not specified.',
3162
+ multiviewChannel: 'Requires starting and stopping the multiview stream from the web interface.',
3163
+ freeGames: 'Only includes games marked as free. Blackouts still apply. Channels/games subject to blackout will be omitted by default.',
3164
+ includeOrgs: 'Including an organization (by MLB team abbreviation, in a comma-separated list if more than 1) will include all of its affiliate broadcasts, or if that affiliate is not broadcasting the game, it will include the opponent broadcast if available. If this option is not specified, but favorite team(s) have been provided, affiliate games for those organizations will be included anyway.',
3165
+ includeLevels: '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.',
3166
+ includeTeamsInTitles: 'Adds an optional parameter to include team names in the ICS/XML titles. A value of "channels" will format the titles in the style of the legacy Channels container.',
3167
+ offAir: 'Adds an optional parameter to 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 legacy Channels container.'
3168
+ }
3169
+
3170
+ function channelInfo(label, id) {
3171
+ return `<div class="channelLabel"><span>${label}</span><div class="info infoPadding" data-target="#${id}">?</div></div><div id="${id}" class="infoContainer"><div class="infoContent">${channelInfoText[id]}</div></div>`
3172
+ }
3173
+
3174
+ body += `
3175
+ <div class="section">`
3176
+
3177
+ body += '<div class="menuContainer"><div class="cardMenuHeader">Live Channel Playlist, XMLTV Guide, ICS Calendar</div><div class="menuContent channelContent">' + "\n"
3178
+ body += '<span> Allows you to generate a M3U playlist of channels, and an XML file of guide listings for those channels, to import into TV/DVR/PVR software like Tvheadend or Jellyfin. You can also subscribe to the calendar links in your preferred calendar program/service to set up event notifications. NOTE: May be helpful to specify a resolution above.</span>'
3179
+
3180
+ body += '<div class="channelScanMode"><div>' + channelInfo('Scan Mode', 'scanMode') + '</div><div class="channelLinks">'
2634
3181
  for (var i = 0; i < VALID_SCAN_MODES.length; i++) {
2635
3182
  body += '<button '
2636
3183
  if ( scan_mode == VALID_SCAN_MODES[i] ) body += 'class="default" '
2637
3184
  body += 'onclick="scan_mode=\'' + VALID_SCAN_MODES[i] + '\';reload()">' + VALID_SCAN_MODES[i] + '</button> '
2638
3185
  }
2639
- body += ' <span class="tinytext">(ON plays sample for all stream requests)</span></p>' + "\n"
3186
+ body += ' <span class="channelNote">(ON plays sample for all stream requests)</span></div></div>' + "\n"
2640
3187
 
2641
3188
  if ( !req.query.resolution ) {
2642
3189
  resolution = 'best'
2643
3190
  }
2644
3191
 
2645
- 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 MASN, 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"
3192
+ body += '<div class="channelRow"><div>' + channelInfo('All', 'allChannels') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + content_protect_b + '">ics</a></div></div>' + "\n"
2646
3193
 
2647
3194
  let include_teams = 'ath,atl'
2648
3195
  if ( (session.credentials.fav_teams.length > 0) && (session.credentials.fav_teams[0].length > 0) ) {
2649
3196
  include_teams = session.credentials.fav_teams.toString()
2650
3197
  }
2651
- 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"
3198
+ body += '<div class="channelRow"><div>' + channelInfo('By team', 'byTeam') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=' + include_teams + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + content_protect_b + '">ics</a></div></div>' + "\n"
2652
3199
 
2653
- body += '<p><span class="tooltip">By team w/ radio<span class="tooltiptext">Same as above, but defaults to that team\'s radio track, if available.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=' + include_teams + '&audio_track=radio' + content_protect_b + '">channels.m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&audio_track=radio' + content_protect_b + '">guide.xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&audio_track=radio' + content_protect_b + '">calendar.ics</a></p>' + "\n"
3200
+ body += '<div class="channelRow"><div>' + channelInfo('By team w/ radio', 'byTeamRadio') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=' + include_teams + '&audio_track=radio' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&audio_track=radio' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&audio_track=radio' + content_protect_b + '">ics</a></div></div>' + "\n"
2654
3201
 
2655
- body += '<p><span class="tooltip">By team w/ Spanish<span class="tooltiptext">Same as above, but defaults to that team\'s Spanish radio track, if available.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=' + include_teams + '&audio_track=spanish' + content_protect_b + '">channels.m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&audio_track=spanish' + content_protect_b + '">guide.xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&audio_track=spanish' + content_protect_b + '">calendar.ics</a></p>' + "\n"
3202
+ body += '<div class="channelRow"><div>' + channelInfo('By team w/ Spanish', 'byTeamSpanish') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=' + include_teams + '&audio_track=spanish' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&audio_track=spanish' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&audio_track=spanish' + content_protect_b + '">ics</a></div></div>' + "\n"
2656
3203
 
2657
- 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"
3204
+ body += '<div class="channelRow"><div>' + channelInfo('Include blackouts', 'includeBlackouts') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=' + include_teams + '&includeBlackouts=true' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeBlackouts=true' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeBlackouts=true' + content_protect_b + '">ics</a></div></div>' + "\n"
2658
3205
 
2659
3206
  let exclude_teams = 'ath,atl'
2660
- 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"
3207
+ body += '<div class="channelRow"><div>' + channelInfo('Exclude a team', 'excludeTeam') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&excludeTeams=' + exclude_teams + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">ics</a></div></div>' + "\n"
2661
3208
 
2662
- body += '<p><span class="tooltip">Include (or exclude) Winter Leagues<span class="tooltiptext">Winter leagues include the Arizona Fall League, Dominican Winter League aka Liga de Beisbol Dominicano, and Mexican Winter League aka Liga Mexicana del Pacífico. 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=winter' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=winter' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=winter' + content_protect_b + '">ics</a></p>' + "\n"
3209
+ body += '<div class="channelRow"><div>' + channelInfo('Include (or exclude) Winter Leagues', 'winterLeagues') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=winter' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=winter' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=winter' + content_protect_b + '">ics</a></div></div>' + "\n"
2663
3210
 
2664
3211
  if ( entitlements.includes('MASN_110') ) {
2665
- body += '<p><span class="tooltip">Include (or exclude) MASN<span class="tooltiptext">MASN live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/MASN-In-Market-Offering">See here for more information</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=masn' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=masn' + content_protect_b + '">xml</a></p>' + "\n"
3212
+ body += '<div class="channelRow"><div>' + channelInfo('Include (or exclude) MASN', 'masn') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=masn' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=masn' + content_protect_b + '">xml</a></div></div>' + "\n"
2666
3213
  }
2667
3214
 
2668
3215
  if ( entitlements.includes('MLBN') || entitlements.includes('EXECMLB') || entitlements.includes('MLBTVMLBNADOBEPASS') ) {
2669
- body += '<p><span class="tooltip">Include (or exclude) MLB Network<span class="tooltiptext">MLB Network live stream is now available in the USA for paid MLBTV subscribers or as a paid add-on, in addition to authenticated TV subscribers. <a href="https://support.mlb.com/s/article/MLB-Network-Streaming-FAQ">See here for more information</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=mlbn' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=mlbn' + content_protect_b + '">xml</a></p>' + "\n"
3216
+ body += '<div class="channelRow"><div>' + channelInfo('Include (or exclude) MLB Network', 'mlbn') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=mlbn' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=mlbn' + content_protect_b + '">xml</a></div></div>' + "\n"
2670
3217
  }
2671
3218
 
2672
3219
  if ( entitlements.includes('SNLA_119') ) {
2673
- body += '<p><span class="tooltip">Include (or exclude) SportsNet LA<span class="tooltiptext">SNLA live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/SNLA-Plus-Subscription-Packages">See here for more information</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=snla' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=snla' + content_protect_b + '">xml</a></p>' + "\n"
3220
+ body += '<div class="channelRow"><div>' + channelInfo('Include (or exclude) SportsNet LA', 'snla') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=snla' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=snla' + content_protect_b + '">xml</a></div></div>' + "\n"
2674
3221
  }
2675
3222
 
2676
3223
  if ( entitlements.includes('SNY_121') ) {
2677
- body += '<p><span class="tooltip">Include (or exclude) SNY<span class="tooltiptext">SNY live stream for entitled subscribers. <a href="https://support.mlb.com/s/article/SNY-In-Market-Offering">See here for more information</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=sny' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=sny' + content_protect_b + '">xml</a></p>' + "\n"
3224
+ body += '<div class="channelRow"><div>' + channelInfo('Include (or exclude) SNY', 'sny') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=sny' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=sny' + content_protect_b + '">xml</a></div></div>' + "\n"
2678
3225
  }
2679
3226
 
2680
- body += '<p><span class="tooltip">Include (or exclude) Big Inning<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>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=biginning' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=biginning' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=biginning' + content_protect_b + '">ics</a></p>' + "\n"
3227
+ body += '<div class="channelRow"><div>' + channelInfo('Include (or exclude) Big Inning', 'bigInning') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=biginning' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=biginning' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=biginning' + content_protect_b + '">ics</a></div></div>' + "\n"
2681
3228
 
2682
3229
  let gamechanger_resolution = resolution
2683
3230
  if ( gamechanger_resolution == VALID_RESOLUTIONS[0] ) {
2684
3231
  gamechanger_resolution = 'best'
2685
3232
  }
2686
- body += '<p><span class="tooltip">Include (or exclude) Game Changer<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>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + gamechanger_resolution + '&includeTeams=gamechanger' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=gamechanger' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=gamechanger' + content_protect_b + '">ics</a></p>' + "\n"
3233
+ body += '<div class="channelRow"><div>' + channelInfo('Include (or exclude) Game Changer', 'gameChanger') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + gamechanger_resolution + '&includeTeams=gamechanger' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=gamechanger' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=gamechanger' + content_protect_b + '">ics</a></div></div>' + "\n"
2687
3234
 
2688
- body += '<p><span class="tooltip">Include (or exclude) Stream Finder<span class="tooltiptext">The stream finder stream will automatically switch between games according to your uploaded preferences. This stream is not affiliated with Baseball Reference, do not contact them for support. Visit <a href="http://bit.ly/bbrefsf">http://bit.ly/bbrefsf</a> to create and export your preferences, then upload and save them to mlbserver <a href="#streamfinder">above</a>. Does not support adaptive bitrate switching, will default to best resolution if not specified.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + gamechanger_resolution + '&includeTeams=streamfinder' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=streamfinder' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=streamfinder' + content_protect_b + '">ics</a></p>' + "\n"
3235
+ body += '<div class="channelRow"><div>' + channelInfo('Include (or exclude) Stream Finder', 'streamFinder') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + gamechanger_resolution + '&includeTeams=streamfinder' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=streamfinder' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=streamfinder' + content_protect_b + '">ics</a></div></div>' + "\n"
2689
3236
 
2690
- body += '<p><span class="tooltip">Include (or exclude) Multiview<span class="tooltiptext">Requires starting and stopping the multiview stream from the web interface.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&includeTeams=multiview' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=multiview' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=multiview' + content_protect_b + '">ics</a></p>' + "\n"
3237
+ body += '<div class="channelRow"><div>' + channelInfo('Include (or exclude) Multiview', 'multiviewChannel') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&includeTeams=multiview' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=multiview' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=multiview' + content_protect_b + '">ics</a></div></div>' + "\n"
2691
3238
 
2692
3239
  if ( argv.free ) {
2693
- body += '<p><span class="tooltip">Free games only<span class="tooltiptext">Only includes games marked as free. Blackouts still apply. Channels/games subject to blackout will be omitted by default.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=free' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=free' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=free' + content_protect_b + '">ics</a></p>' + "\n"
3240
+ body += '<div class="channelRow"><div>' + channelInfo('Free games only', 'freeGames') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=free' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=free' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=free' + content_protect_b + '">ics</a></div></div>' + "\n"
2694
3241
  }
2695
3242
 
2696
- body += '<p><span class="tooltip">Include affiliates by org<span class="tooltiptext">Including an organization (by MLB team abbreviation, in a comma-separated list if more than 1) will include all of its affiliate broadcasts, or if that affiliate is not broadcasting the game, it will include the opponent\'s broadcast if available. If this option is not specified, but favorite team(s) have been provided, affiliate games for those organizations will be included anyway.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeOrgs=ath,atl' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeOrgs=ath' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeOrgs=ath' + content_protect_b + '">ics</a></p>' + "\n"
2697
-
2698
- 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"
3243
+ body += '<div class="channelRow"><div>' + channelInfo('Include affiliates by org', 'includeOrgs') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeOrgs=ath,atl' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeOrgs=ath' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeOrgs=ath' + content_protect_b + '">ics</a></div></div>' + "\n"
2699
3244
 
2700
- 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"
3245
+ body += '<div class="channelRow"><div>' + channelInfo('Include by level', 'includeLevels') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeLevels=a%2B,aaa' + content_protect_b + '">m3u</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeLevels=a%2B,aaa' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeLevels=a%2B,aaa' + content_protect_b + '">ics</a></div></div>' + "\n"
2701
3246
 
2702
- 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"
3247
+ body += '<div class="channelRow"><div>' + channelInfo('Include teams in titles', 'includeTeamsInTitles') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=channels' + content_protect_b + '">legacy</a><a class="channelLink" href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">ics</a></div></div>' + "\n"
2703
3248
 
2704
- body += '</td></tr></table><br/>' + "\n"
3249
+ body += '<div class="channelRow"><div>' + channelInfo('Create Off Air events between games', 'offAir') + '</div><div class="channelLinks"><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&offAir=true' + content_protect_b + '">xml</a><a class="channelLink" href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&offAir=channels' + content_protect_b + '">legacy</a></div></div>' + "\n"
3250
+ body += '</div></div>' + "\n"
2705
3251
 
2706
- body += '<table><tr><td>' + "\n"
2707
- body += '<p><span class="tooltip">Example links<span class="tooltiptext">Some examples how to generate predictable links.</span></span>:</p>' + "\n"
2708
- body += '<p>' + "\n"
3252
+ body += '<div class="menuContainer"><div class="cardMenuHeader">Example links</div><div class="menuContent">'
2709
3253
  let example_types = [ ['embed.html', 'Embed'], ['stream.m3u8', 'Stream'], ['chromecast.html', 'Chromecast'], ['kodi.strm', 'Kodi'] ]
2710
3254
 
2711
3255
  let example_team = 'ath'
@@ -2743,10 +3287,19 @@ app.get('/', async function(req, res) {
2743
3287
  }
2744
3288
  }
2745
3289
  }
2746
- body += '</p>' + "\n"
2747
3290
 
2748
3291
  include_teams = 'ath,atl'
2749
- 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"
3292
+ body += `<br>
3293
+ <div class="is-flex">
3294
+ <span>Game Changer by team examples:</span>
3295
+ <div class="info infoPadding" data-target="#gameChangerExamplesInfo">?</div>
3296
+ </div>
3297
+ <div id="gameChangerExamplesInfo" class="infoContainer">
3298
+ <div class="infoContent">
3299
+ Game Changer supports specifying certain teams to include or exclude. Useful for following a group of teams.
3300
+ </div>
3301
+ </div>
3302
+ `
2750
3303
  body += '<p>' + "\n"
2751
3304
  let gamechanger_streamURL = server + '/gamechanger.m3u8?resolution=best' + content_protect_b
2752
3305
  let gamechanger_types = ['in', 'ex']
@@ -2754,19 +3307,65 @@ app.get('/', async function(req, res) {
2754
3307
  let example_streamURL = gamechanger_streamURL + '&' + gamechanger_types[i] + 'cludeTeams=' + include_teams
2755
3308
  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"
2756
3309
  }
3310
+ body += '</p>' + "\n"
2757
3311
 
2758
- body += '<p><span class="tooltip">Comskip link examples<span class="tooltiptext">You can generate a <a href="https://github.com/erikkaashoek/Comskip">Comskip</a>-style file to automatically skip sections (breaks, idle time, or non-action pitches) of games you record using DVR software when watched in compatible players. For example, if you record a game from your local OTA channel using Tvheadend, you can then fetch one of these Comskip files, put it in the same directory with the same name as your recorded video file, and Kodi will automatically skip those sections while you watch the video.<br><br>Specifying the team and broadcast_start_timestamp in the URL is required! For the timestamp, use the time your DVR software began the recording. This should be your local time in YYYY-MM-DDTHH:MM:SS format.<br><br>Specifying a skip_adjust value in the URL is recommended, to adjust for broadcast delays. This will vary across different channels and different video sources.<br><br>For the txt file format, specifying the video frame rate (fps) in the URL is also required. This will commonly be either 30, 59.94, or 60, depending on your video source.<br><br>Optionally, setting pad to "on" will generate random extra skips at the end, to help avoid timeline spoilers.</span></span>: <a href="' + http_root + '/comskip.edl?team=CHC&date=2025-10-01&pad=on&skip=pitches&skip_adjust=11&broadcast_start_timestamp=2025-10-01T14:00:00' + content_protect_a + '">comskip.edl</a> or <a href="' + http_root + '/comskip.txt?team=CHC&date=2025-10-01&pad=on&skip=pitches&skip_adjust=11&broadcast_start_timestamp=2025-10-01T14:00:00&fps=59.94' + content_protect_a + '">comskip.txt</a></p>' + "\n"
3312
+ body += `
3313
+ <div class="is-flex">
3314
+ <span>Comskip link examples:</span>
3315
+ <div class="info infoPadding" data-target="#comskipExamplesInfo">?</div>
3316
+ </div>
3317
+ <div id="comskipExamplesInfo" class="infoContainer">
3318
+ <div class="infoContent">
3319
+ You can generate a <a href="https://github.com/erikkaashoek/Comskip">Comskip</a>-style file to automatically skip sections (breaks, idle time, or non-action pitches) of games you record using DVR software when watched in compatible players. For example, if you record a game from your local OTA channel using Tvheadend, you can then fetch one of these Comskip files, put it in the same directory with the same name as your recorded video file, and Kodi will automatically skip those sections while you watch the video.<br><br>Specifying the team and broadcast_start_timestamp in the URL is required! For the timestamp, use the time your DVR software began the recording. This should be your local time in YYYY-MM-DDTHH:MM:SS format.<br><br>Specifying a skip_adjust value in the URL is recommended, to adjust for broadcast delays. This will vary across different channels and different video sources.<br><br>For the txt file format, specifying the video frame rate (fps) in the URL is also required. This will commonly be either 30, 59.94, or 60, depending on your video source.<br><br>Optionally, setting pad to "on" will generate random extra skips at the end, to help avoid timeline spoilers.
3320
+ </div>
3321
+ </div>
3322
+ <p><a href="${http_root}/comskip.edl?team=CHC&date=2025-10-01&pad=on&skip=pitches&skip_adjust=11&broadcast_start_timestamp=2025-10-01T14:00:00${content_protect_a}">comskip.edl</a> or <a href="${http_root}/comskip.txt?team=CHC&date=2025-10-01&pad=on&skip=pitches&skip_adjust=11&broadcast_start_timestamp=2025-10-01T14:00:00&fps=59.94${content_protect_a}">comskip.txt</a></p>
3323
+ `
2759
3324
 
2760
- body += '<p><span class="tooltip">MPEG-TS examples<span class="tooltiptext">Experimental feature: a MPEGTS output format where you can adjust the audio sync with a URL parameter. Useful if the radio track is a consistent number of seconds ahead or behind the video track. Use positive sync values if radio is early, or negative values if radio is late.</span></span>: <a href="' + http_root + '/stream.ts?team=' + example_team + content_protect_a + '">Stream</a> or <a href="' + http_root + '/stream.ts?team=' + example_team + '&audio_track=radio&sync=2.3' + content_protect_a + '">Stream w/ radio sync</a></p>' + "\n"
2761
-
2762
- body += '</p></td></tr></table><br/>' + "\n"
2763
-
2764
- body += '<p><span class="tooltip">Sample video<span class="tooltiptext">A sample stream. Useful for testing and troubleshooting.</span></span>: <a href="' + http_root + '/embed.html' + content_protect_a + '">Embed</a> | <a href="' + http_root + '/stream.m3u8' + content_protect_a + '">Stream</a> | <a href="' + http_root + '/chromecast.html' + content_protect_a + '">Chromecast</a> | <a href="' + http_root + '/advanced.html' + content_protect_a + '">Advanced</a></p>' + "\n"
2765
-
2766
- body += '<p><span class="tooltip">Bookmarklets for MLB.com<span class="tooltiptext">If you watch at MLB.com, drag these bookmarklets to your bookmarks toolbar and use them to hide parts of the interface.</span></span>: <a href="javascript:(function(){let x=document.querySelector(\'#mlbtv-stats-panel\');if(x.style.display==\'none\'){x.style.display=\'initial\';}else{x.style.display=\'none\';}})();">Boxscore</a> | <a href="javascript:(function(){let x=document.querySelector(\'.mlbtv-header-container\');if(x.style.display==\'none\'){let y=document.querySelector(\'.mlbtv-players-container\');y.style.display=\'none\';x.style.display=\'initial\';setTimeout(function(){y.style.display=\'initial\';},15);}else{x.style.display=\'none\';}})();">Scoreboard</a> | <a href="javascript:(function(){let x=document.querySelector(\'.mlbtv-container--footer\');if(x.style.display==\'none\'){let y=document.querySelector(\'.mlbtv-players-container\');y.style.display=\'none\';x.style.display=\'initial\';setTimeout(function(){y.style.display=\'initial\';},15);}else{x.style.display=\'none\';}})();">Linescore</a> | <a href="javascript:(function(){let x=document.querySelector(\'#mlbtv-stats-panel\');if(x.style.display==\'none\'){x.style.display=\'initial\';}else{x.style.display=\'none\';}x=document.querySelector(\'.mlbtv-header-container\');if(x.style.display==\'none\'){x.style.display=\'initial\';}else{x.style.display=\'none\';}x=document.querySelector(\'.mlbtv-container--footer\');if(x.style.display==\'none\'){let y=document.querySelector(\'.mlbtv-players-container\');y.style.display=\'none\';x.style.display=\'initial\';setTimeout(function(){y.style.display=\'initial\';},15);}else{x.style.display=\'none\';}})();">All</a></p>' + "\n"
3325
+ body += `
3326
+ <div class="is-flex">
3327
+ <span>MPEG-TS examples:</span>
3328
+ <div class="info infoPadding" data-target="#mpegTsExamplesInfo">?</div>
3329
+ </div>
3330
+ <div id="mpegTsExamplesInfo" class="infoContainer">
3331
+ <div class="infoContent">
3332
+ Experimental feature: a MPEGTS output format where you can adjust the audio sync with a URL parameter. Useful if the radio track is a consistent number of seconds ahead or behind the video track. Use positive sync values if radio is early, or negative values if radio is late.
3333
+ </div>
3334
+ </div>
3335
+ <p><a href="${http_root}/stream.ts?team=${example_team}${content_protect_a}">Stream</a> or <a href="${http_root}/stream.ts?team=${example_team}&audio_track=radio&sync=2.3${content_protect_a}">Stream w/ radio sync</a></p>
3336
+ `
3337
+
3338
+ body += '<br/>' + "\n"
3339
+
3340
+ body += `
3341
+ <div class="is-flex">
3342
+ <span>Sample video:</span>
3343
+ <div class="info infoPadding" data-target="#sampleVideoInfo">?</div>
3344
+ </div>
3345
+ <div id="sampleVideoInfo" class="infoContainer">
3346
+ <div class="infoContent">
3347
+ A sample stream. Useful for testing and troubleshooting.
3348
+ </div>
3349
+ </div>
3350
+ <p><a href="${http_root}/embed.html${content_protect_a}">Embed</a> | <a href="${http_root}/stream.m3u8${content_protect_a}">Stream</a> | <a href="${http_root}/chromecast.html${content_protect_a}">Chromecast</a> | <a href="${http_root}/advanced.html${content_protect_a}">Advanced</a></p>
3351
+ `
3352
+
3353
+ body += `
3354
+ <div class="is-flex">
3355
+ <span>Bookmarklets for MLB.com:</span>
3356
+ <div class="info infoPadding" data-target="#bookmarkletsInfo">?</div>
3357
+ </div>
3358
+ <div id="bookmarkletsInfo" class="infoContainer">
3359
+ <div class="infoContent">
3360
+ If you watch at MLB.com, drag these bookmarklets to your bookmarks toolbar and use them to hide parts of the interface.
3361
+ </div>
3362
+ </div>
3363
+ `
3364
+ body += '<p><a href="javascript:(function(){let x=document.querySelector(\'#mlbtv-stats-panel\');if(x.style.display==\'none\'){x.style.display=\'initial\';}else{x.style.display=\'none\';}})();">Boxscore</a> | <a href="javascript:(function(){let x=document.querySelector(\'.mlbtv-header-container\');if(x.style.display==\'none\'){let y=document.querySelector(\'.mlbtv-players-container\');y.style.display=\'none\';x.style.display=\'initial\';setTimeout(function(){y.style.display=\'initial\';},15);}else{x.style.display=\'none\';}})();">Scoreboard</a> | <a href="javascript:(function(){let x=document.querySelector(\'.mlbtv-container--footer\');if(x.style.display==\'none\'){let y=document.querySelector(\'.mlbtv-players-container\');y.style.display=\'none\';x.style.display=\'initial\';setTimeout(function(){y.style.display=\'initial\';},15);}else{x.style.display=\'none\';}})();">Linescore</a> | <a href="javascript:(function(){let x=document.querySelector(\'#mlbtv-stats-panel\');if(x.style.display==\'none\'){x.style.display=\'initial\';}else{x.style.display=\'none\';}x=document.querySelector(\'.mlbtv-header-container\');if(x.style.display==\'none\'){x.style.display=\'initial\';}else{x.style.display=\'none\';}x=document.querySelector(\'.mlbtv-container--footer\');if(x.style.display==\'none\'){let y=document.querySelector(\'.mlbtv-players-container\');y.style.display=\'none\';x.style.display=\'initial\';setTimeout(function(){y.style.display=\'initial\';},15);}else{x.style.display=\'none\';}})();">All</a></p>' + "\n"
2767
3365
 
2768
3366
  // Print version
2769
3367
  body += '<p class="tinytext">Version ' + version + ' (<a href="' + http_root + '/clearcache">clear session and cache</a>)</p>' + "\n"
3368
+ body += '</div></div></div>'
2770
3369
 
2771
3370
  // Datepicker functions
2772
3371
  body += '<script>var datePicker=document.getElementById("gameDate");function changeDate(e){date=datePicker.value;reload()}function removeDate(e){datePicker.removeEventListener("change",changeDate,false);datePicker.addEventListener("blur",changeDate,false);if(e.keyCode===13){date=datePicker.value;reload()}}datePicker.addEventListener("change",changeDate,false);datePicker.addEventListener("keypress",removeDate,false)</script>' + "\n"
@@ -2776,6 +3375,33 @@ app.get('/', async function(req, res) {
2776
3375
 
2777
3376
  // Highlights modal functions
2778
3377
  body += `<script type="text/javascript">
3378
+ `
3379
+ // Highlights modal positioning
3380
+ body += `var pageScrollY = 0;
3381
+
3382
+ function openModal() {
3383
+ pageScrollY = window.scrollY || document.documentElement.scrollTop;
3384
+ document.body.style.position = "fixed";
3385
+ document.body.style.top = "-" + pageScrollY + "px";
3386
+ document.body.style.left = "0";
3387
+ document.body.style.right = "0";
3388
+ document.body.style.width = "100%";
3389
+ document.body.style.overflow = "hidden";
3390
+ modal.style.display = "flex";
3391
+ }
3392
+
3393
+ function closeModal() {
3394
+ document.body.style.position = "";
3395
+ document.body.style.top = "";
3396
+ document.body.style.left = "";
3397
+ document.body.style.right = "";
3398
+ document.body.style.width = "";
3399
+ document.body.style.overflow = "";
3400
+ window.scrollTo(0, pageScrollY);
3401
+ modal.style.display = "none";
3402
+ }
3403
+ `
3404
+ body += `
2779
3405
  var modal = document.getElementById("myModal");
2780
3406
  var highlightsModal = document.getElementById("highlights");
2781
3407
  var span = document.getElementsByClassName("close")[0];
@@ -2807,7 +3433,7 @@ function parsehighlightsresponse(responsetext) {
2807
3433
  }
2808
3434
  modaltext += "</ul>";
2809
3435
  highlightsModal.innerHTML = modaltext;
2810
- modal.style.display = "block"
3436
+ openModal();
2811
3437
  } catch (e) {
2812
3438
  alert("Error processing highlights: " + e.message)
2813
3439
  }
@@ -2816,12 +3442,77 @@ function showhighlights(gamePk, gameDate) {
2816
3442
  makeGETRequest("` + http_root + `/highlights?gamePk=" + gamePk + "&gameDate=" + gameDate + "` + content_protect_b + `", parsehighlightsresponse);
2817
3443
  return false
2818
3444
  }
2819
- span.onclick = function() {
2820
- modal.style.display = "none";
2821
- }
2822
- `
3445
+ span.onclick = closeModal;`
3446
+
3447
+ body += 'window.onclick = function(event) { if (event.target == modal) {closeModal()} }</script>' + "\n"
3448
+
3449
+ // Find recaps and condensed games from highlights
3450
+ body += `<script type="text/javascript">
3451
+
3452
+ function parseHighlight(responsetext, keyword) {
3453
+ try {
3454
+ var highlights = JSON.parse(responsetext);
3455
+ var captions_parameter = '';
3456
+ if (captions == 'disabled') {
3457
+ captions_parameter = '&captions=disabled';
3458
+ }
3459
+ var highlightExists = null;
3460
+ var hls_url = '';
3461
+ var mp4_url = '';
3462
+ if (highlights && (highlights.length > 0)) {
3463
+
3464
+ for (var i = 0; i < highlights.length; i++) {
3465
+ var keywordsAll = highlights[i].keywordsAll || [];
3466
+
3467
+ var hasKeyword = keywordsAll.some(function(item) {
3468
+ return item && (item.value && item.value === keyword)
3469
+ });
3470
+ if (highlights[i].headline && hasKeyword) {
3471
+ highlightExists = highlights[i];
3472
+ for (var j = 0; j < highlights[i].playbacks.length; j++) {
3473
+ if (highlights[i].playbacks[j].name === 'HTTP_CLOUD_WIRED_60') {
3474
+ hls_url = highlights[i].playbacks[j].url;
3475
+ } else if (highlights[i].playbacks[j].name === 'mp4Avc') {
3476
+ mp4_url = highlights[i].playbacks[j].url;
3477
+ }
3478
+ }
3479
+ break;
3480
+ }
3481
+ }
3482
+ if (!highlightExists || !hls_url) {
3483
+ toast();
3484
+ return;
3485
+ } { window.location.href = "` + link + `?highlight_src=" + encodeURIComponent(hls_url) + "&resolution=" + resolution + captions_parameter + "` + content_protect_b + `"; }
3486
+
3487
+
3488
+ }
3489
+ } catch (e) {
3490
+
3491
+ {toast()}
3492
+ }
3493
+ }
3494
+
3495
+ function showHighlightByKeyword(gamePk, gameDate, keyword) {
3496
+ makeGETRequest(
3497
+ "` + http_root + `/highlights?gamePk=" + gamePk + "&gameDate=" + gameDate + "` + content_protect_b + `",
3498
+ function(response) {
3499
+ parseHighlight(response, keyword);
3500
+ }
3501
+ );
3502
+ return false;
3503
+ }
3504
+
3505
+ function showcondensed(gamePk, gameDate) {
3506
+ return showHighlightByKeyword(gamePk, gameDate, 'MLBCOM_CONDENSED_GAME');
3507
+ }
3508
+
3509
+ function showrecap(gamePk, gameDate) {
3510
+ return showHighlightByKeyword(gamePk, gameDate, 'MLBCOM_GAME_RECAP');
3511
+ }
2823
3512
 
2824
- body += 'window.onclick = function(event) { if (event.target == modal) { modal.style.display = "none"; } }</script>' + "\n"
3513
+ </script>`
3514
+ body +=`</div>`
3515
+ body += '<div id="snackbar">Not yet available. Come back later.</div>'
2825
3516
 
2826
3517
  body += "</body></html>"
2827
3518
 
@@ -3331,6 +4022,7 @@ app.get('/highlights', async function(req, res) {
3331
4022
  if ( req.query.gamePk && req.query.gameDate ) {
3332
4023
  highlightsData = await session.getHighlightsData(req.query.gamePk, req.query.gameDate)
3333
4024
  }
4025
+ res.writeHead(200, {'Content-Type': 'application/json; charset=utf-8'});
3334
4026
  res.end(JSON.stringify(highlightsData))
3335
4027
  } catch (e) {
3336
4028
  session.log('highlights request error : ' + e.message)