mlbserver 2025.2.16 → 2025.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/index.js +77 -59
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -52,6 +52,7 @@ services:
52
52
  #- page_password=
53
53
  #- content_protect=
54
54
  #- gamechanger_delay=0
55
+ #- http_root=/mlbserver
55
56
  ports:
56
57
  - 9999:9999
57
58
  - 10000:10000
@@ -111,6 +112,7 @@ Advanced command line or Docker environment options:
111
112
  --page_password (password to protect pages; default is no protection)
112
113
  --content_protect (specify the content protection key to include as a URL parameter, if page protection is enabled)
113
114
  --gamechanger_delay (specify extra delay for the gamechanger switches in 10 second increments, default is 0)
115
+ --http_root (specify the alternative http webroot or initial path prefix, default is none)
114
116
  ```
115
117
 
116
118
  For multiview, the default software encoder is limited by your CPU. You may want to experiment with different ffmpeg hardware encoders. "h264_videotoolbox" is confirmed to work on supported Macs, and "h264_v4l2m2m" is confirmed to work on a Raspberry Pi 4 (and likely other Linux systems) when ffmpeg is compiled with this patch: https://www.raspberrypi.org/forums/viewtopic.php?p=1780625#p1780625
package/index.js CHANGED
@@ -29,10 +29,10 @@ const VALID_CONTROLS = [ 'Show', 'Hide' ]
29
29
  const VALID_INNING_HALF = [ '', 'top', 'bottom' ]
30
30
  const VALID_INNING_NUMBER = [ '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12' ]
31
31
  const VALID_SCORES = [ 'Hide', 'Show' ]
32
- const VALID_RESOLUTIONS = [ 'adaptive', '720p60', '720p', '540p', '504p', '360p', 'none' ]
32
+ const VALID_RESOLUTIONS = [ 'adaptive', '1080p60', '720p60', '720p', '540p', '504p', '360p', '288p', '214p', 'none' ]
33
33
  const DEFAULT_MULTIVIEW_RESOLUTION = '504p'
34
34
  // Corresponding andwidths to display for above resolutions
35
- const DISPLAY_BANDWIDTHS = [ '', '6600k', '4160k', '2950k', '2120k', '1400k', '' ]
35
+ const DISPLAY_BANDWIDTHS = [ '', '9600k', '6600k', '4160k', '2950k', '2120k', '1400k', '1000k', '600k', '' ]
36
36
  const VALID_AUDIO_TRACKS = [ 'all', 'English', 'Home Radio', 'Casa Radio', 'Away Radio', 'Visita Radio', 'Park', 'none' ]
37
37
  const DISPLAY_AUDIO_TRACKS = [ 'all', 'TV', 'Radio', 'Spa.', 'Away Rad.', 'Away Sp.', 'Park', 'none' ]
38
38
  const DEFAULT_MULTIVIEW_AUDIO_TRACK = 'English'
@@ -103,7 +103,7 @@ var argv = minimist(process.argv, {
103
103
  e: 'env'
104
104
  },
105
105
  boolean: ['ffmpeg_logging', 'debug', 'logout', 'session', 'cache', 'version', 'free', 'env'],
106
- string: ['account_username', 'account_password', 'fav_teams', 'multiview_path', 'ffmpeg_path', 'ffmpeg_encoder', 'page_username', 'page_password', 'content_protect', 'data_directory']
106
+ string: ['account_username', 'account_password', 'fav_teams', 'multiview_path', 'ffmpeg_path', 'ffmpeg_encoder', 'page_username', 'page_password', 'content_protect', 'data_directory', 'http_root']
107
107
  })
108
108
 
109
109
  if (argv.env) argv = process.env
@@ -151,6 +151,22 @@ const ffmpegEncoder = argv.ffmpeg_encoder || defaultEncoder
151
151
  // Declare web server
152
152
  var app = root()
153
153
 
154
+ var http_root = '';
155
+ if (argv.http_root) {
156
+ http_root = argv.http_root
157
+ app.get(http_root + '/*', async function(req, res) {
158
+ if ( ! (await protect(req, res)) ) return
159
+
160
+ try {
161
+ req.url = req.url.substr(http_root.length, (req.url.length - http_root.length))
162
+ app.route(req, res);
163
+ } catch (e) {
164
+ session.log('http_root request error : ' + e.message)
165
+ res.end('http_root request error, check log')
166
+ }
167
+ })
168
+ }
169
+
154
170
  // Get appname from directory
155
171
  var appname = path.basename(__dirname)
156
172
 
@@ -202,7 +218,7 @@ app.get('/clearcache', async function(req, res) {
202
218
  session.clear_session_data()
203
219
  session = new sessionClass(argv)
204
220
 
205
- let server = 'http://' + req.headers.host
221
+ let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
206
222
  res.redirect(server)
207
223
  } catch (e) {
208
224
  session.log('clearcache request error : ' + e.message)
@@ -231,11 +247,7 @@ app.get('/stream.m3u8', async function(req, res) {
231
247
  streamURL = SAMPLE_STREAM_URL
232
248
  options.referer = 'https://hls-js-dev.netlify.app/'
233
249
  } else {
234
- if ( req.query.resolution && (req.query.resolution == 'best') ) {
235
- options.resolution = VALID_RESOLUTIONS[1]
236
- } else {
237
- options.resolution = session.returnValidItem(req.query.resolution, VALID_RESOLUTIONS)
238
- }
250
+ options.resolution = req.query.resolution
239
251
  options.audio_track = session.returnValidItem(req.query.audio_track, VALID_AUDIO_TRACKS)
240
252
  options.force_vod = req.query.force_vod || VALID_FORCE_VOD[0]
241
253
 
@@ -455,8 +467,15 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
455
467
  var audio_track_matched = false
456
468
  var frame_rate = '29.97'
457
469
  if ( (resolution != VALID_RESOLUTIONS[0]) && (resolution != VALID_RESOLUTIONS[VALID_RESOLUTIONS.length-1]) ) {
458
- if ( resolution.endsWith('p60') ) {
470
+ if ( resolution.endsWith('p60') || (resolution == 'best') ) {
459
471
  frame_rate = '59.94'
472
+ if ( resolution == 'best') {
473
+ if ( body.find(a =>a.includes("x1080,")) ) {
474
+ resolution = VALID_RESOLUTIONS[1]
475
+ } else {
476
+ resolution = VALID_RESOLUTIONS[2]
477
+ }
478
+ }
460
479
  resolution = resolution.slice(0, -3)
461
480
  } else if ( resolution.endsWith('p') ) {
462
481
  resolution = resolution.slice(0, -1)
@@ -480,7 +499,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
480
499
  return line
481
500
  } else if ( segment_found ) {
482
501
  segment_found = false
483
- return '/ts?url='+encodeURIComponent(url.resolve(streamURL, line.trim())) + content_protect + referer_parameter + token_parameter
502
+ return http_root + '/ts?url='+encodeURIComponent(url.resolve(streamURL, line.trim())) + content_protect + referer_parameter + token_parameter
484
503
  }
485
504
 
486
505
  // Omit keyframe tracks
@@ -542,7 +561,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
542
561
  //var parsed = line.match(/URI="([^"]+)"?$/)
543
562
  var parsed = line.match(',URI="([^"]+)"')
544
563
  if ( parsed[1] ) {
545
- newurl = '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim()))
564
+ newurl = http_root + '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim()))
546
565
  if ( force_vod != VALID_FORCE_VOD[0] ) newurl += '&force_vod=on'
547
566
  if ( inning_half != VALID_INNING_HALF[0] ) newurl += '&inning_half=' + inning_half
548
567
  if ( inning_number != VALID_INNING_NUMBER[0] ) newurl += '&inning_number=' + inning_number
@@ -574,17 +593,15 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
574
593
  if ( line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH=') ) {
575
594
  if ( resolution == VALID_RESOLUTIONS[VALID_RESOLUTIONS.length-1] ) {
576
595
  return
596
+ } else if ( resolution === VALID_RESOLUTIONS[0] ) {
597
+ return line
577
598
  } else {
578
- if ( resolution === VALID_RESOLUTIONS[0] ) {
599
+ if ( (video_track_found == false) && (line.indexOf(resolution+',FRAME-RATE='+frame_rate) > 0) ) {
600
+ video_track_matched = true
601
+ video_track_found = true
579
602
  return line
580
603
  } else {
581
- if ( (video_track_found == false) && (line.indexOf(resolution+',FRAME-RATE='+frame_rate) > 0) ) {
582
- video_track_matched = true
583
- video_track_found = true
584
- return line
585
- } else {
586
- return
587
- }
604
+ return
588
605
  }
589
606
  }
590
607
  }
@@ -598,7 +615,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
598
615
  if ( line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="') ) {
599
616
  var parsed = line.match(',URI="([^"]+)"')
600
617
  if ( parsed[1] ) {
601
- newurl = '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim())) + content_protect + referer_parameter + token_parameter
618
+ newurl = http_root + '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim())) + content_protect + referer_parameter + token_parameter
602
619
  return '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="' + newurl + '"'
603
620
  }
604
621
  return
@@ -620,7 +637,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
620
637
  if ( gamePk ) newurl += '&gamePk=' + gamePk
621
638
  if ( audio_track != VALID_AUDIO_TRACKS[0] ) newurl += '&audio_track=' + encodeURIComponent(audio_track)
622
639
  newurl += content_protect + referer_parameter + token_parameter
623
- return '/playlist?url='+newurl
640
+ return http_root + '/playlist?url='+newurl
624
641
  }
625
642
  })
626
643
  .filter(function(line) {
@@ -792,9 +809,9 @@ app.get('/playlist', async function(req, res) {
792
809
 
793
810
  if (line[0] === '#') return line
794
811
 
795
- let newline = '/ts'
812
+ let newline = http_root + '/ts'
796
813
  if ( line.includes('.vtt') ) {
797
- newline = '/vtt'
814
+ newline = http_root + '/vtt'
798
815
  }
799
816
 
800
817
  newline += '?url='+encodeURIComponent(url.resolve(u, line.trim())) + content_protect + referer_parameter + token_parameter
@@ -961,12 +978,12 @@ app.get('/gamechanger.m3u8', async function(req, res) {
961
978
 
962
979
  var resolution
963
980
  if ( req.query.resolution && (req.query.resolution == 'best') ) {
964
- resolution = VALID_RESOLUTIONS[1]
981
+ resolution = VALID_RESOLUTIONS[2]
965
982
  } else {
966
983
  resolution = session.returnValidItem(req.query.resolution, VALID_RESOLUTIONS)
967
984
  }
968
985
  if ( resolution == VALID_RESOLUTIONS[0] ) {
969
- resolution = VALID_RESOLUTIONS[1]
986
+ resolution = VALID_RESOLUTIONS[2]
970
987
  }
971
988
 
972
989
  var includeTeams = ''
@@ -1020,7 +1037,7 @@ app.get('/gamechangerplaylist', async function(req, res) {
1020
1037
  } else {
1021
1038
  var game_changer_title = 'Game changer ' + id + ' '
1022
1039
 
1023
- var resolution = req.query.resolution || VALID_RESOLUTIONS[1]
1040
+ var resolution = req.query.resolution || VALID_RESOLUTIONS[2]
1024
1041
 
1025
1042
  var includeTeams = req.query.includeTeams || []
1026
1043
  if ( includeTeams.length > 0 ) includeTeams = includeTeams.split(',')
@@ -1219,8 +1236,9 @@ app.get('/', async function(req, res) {
1219
1236
 
1220
1237
  session.requestlog('homepage', req)
1221
1238
 
1222
- let server = 'http://' + req.headers.host
1239
+ let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host
1223
1240
  let multiview_server = server.replace(':' + session.data.port, ':' + session.data.multiviewPort)
1241
+ server += http_root
1224
1242
 
1225
1243
  let gameDate = session.liveDate()
1226
1244
  let today = gameDate
@@ -1366,7 +1384,7 @@ app.get('/', async function(req, res) {
1366
1384
  body += 'var date="' + gameDate + '";var level="' + level + '";var org="' + org + '";var mediaType="' + mediaType + '";var resolution="' + resolution + '";var audio_track="' + audio_track + '";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"
1367
1385
 
1368
1386
  // Reload function, called after options change
1369
- body += 'var defaultDate="' + today + '";var curDate=new Date();var utcHours=curDate.getUTCHours();if ((utcHours >= ' + todayUTCHours + ') && (utcHours < ' + YESTERDAY_UTC_HOURS + ')){defaultDate="' + yesterday + '"}function reload(){var newurl="/?";if (date != defaultDate){var urldate=date;if (date == "' + today + '"){urldate="today"}else if (date == "' + yesterday + '"){urldate="yesterday"}newurl+="date="+urldate+"&"}if (level != "' + default_level + '"){newurl+="level="+encodeURIComponent(level)+"&"}if (org != "All"){newurl+="org="+encodeURIComponent(org)+"&"}if (mediaType != "' + VALID_MEDIA_TYPES[0] + '"){newurl+="mediaType="+mediaType+"&"}if (mediaType=="Video"){if (resolution != "' + VALID_RESOLUTIONS[0] + '"){newurl+="resolution="+resolution+"&"}if (audio_track != "' + VALID_AUDIO_TRACKS[0] + '"){newurl+="audio_track="+encodeURIComponent(audio_track)+"&"}else if (resolution == "none"){newurl+="audio_track="+encodeURIComponent("' + VALID_AUDIO_TRACKS[2] + '")+"&"}if (inning_half != "' + VALID_INNING_HALF[0] + '"){newurl+="inning_half="+inning_half+"&"}if (inning_number != "' + VALID_INNING_NUMBER[0] + '"){newurl+="inning_number="+inning_number+"&"}if (skip != "' + VALID_SKIP[0] + '"){newurl+="skip="+skip+"&";if (skip_adjust != "' + DEFAULT_SKIP_ADJUST + '"){newurl+="skip_adjust="+skip_adjust+"&"}}}if (pad != "' + VALID_PAD[0] + '"){newurl+="pad="+pad+"&";}if (linkType != "' + VALID_LINK_TYPES[0] + '"){newurl+="linkType="+linkType+"&"}if (linkType=="' + VALID_LINK_TYPES[0] + '"){if (startFrom != "' + VALID_START_FROM[0] + '"){newurl+="startFrom="+startFrom+"&"}if (controls != "' + VALID_CONTROLS[0] + '"){newurl+="controls="+controls+"&"}}if (linkType=="Stream"){if (force_vod != "' + VALID_FORCE_VOD[0] + '"){newurl+="force_vod="+force_vod+"&"}}if (scores != "' + VALID_SCORES[0] + '"){newurl+="scores="+scores+"&"}if (scan_mode != "' + session.data.scan_mode + '"){newurl+="scan_mode="+scan_mode+"&"}if (content_protect != ""){newurl+="content_protect="+content_protect+"&"}window.location=newurl.substring(0,newurl.length-1)}' + "\n"
1387
+ body += 'var defaultDate="' + today + '";var curDate=new Date();var utcHours=curDate.getUTCHours();if ((utcHours >= ' + todayUTCHours + ') && (utcHours < ' + YESTERDAY_UTC_HOURS + ')){defaultDate="' + yesterday + '"}function reload(){var newurl="' + http_root + '/?";if (date != defaultDate){var urldate=date;if (date == "' + today + '"){urldate="today"}else if (date == "' + yesterday + '"){urldate="yesterday"}newurl+="date="+urldate+"&"}if (level != "' + default_level + '"){newurl+="level="+encodeURIComponent(level)+"&"}if (org != "All"){newurl+="org="+encodeURIComponent(org)+"&"}if (mediaType != "' + VALID_MEDIA_TYPES[0] + '"){newurl+="mediaType="+mediaType+"&"}if (mediaType=="Video"){if (resolution != "' + VALID_RESOLUTIONS[0] + '"){newurl+="resolution="+resolution+"&"}if (audio_track != "' + VALID_AUDIO_TRACKS[0] + '"){newurl+="audio_track="+encodeURIComponent(audio_track)+"&"}else if (resolution == "none"){newurl+="audio_track="+encodeURIComponent("' + VALID_AUDIO_TRACKS[2] + '")+"&"}if (inning_half != "' + VALID_INNING_HALF[0] + '"){newurl+="inning_half="+inning_half+"&"}if (inning_number != "' + VALID_INNING_NUMBER[0] + '"){newurl+="inning_number="+inning_number+"&"}if (skip != "' + VALID_SKIP[0] + '"){newurl+="skip="+skip+"&";if (skip_adjust != "' + DEFAULT_SKIP_ADJUST + '"){newurl+="skip_adjust="+skip_adjust+"&"}}}if (pad != "' + VALID_PAD[0] + '"){newurl+="pad="+pad+"&";}if (linkType != "' + VALID_LINK_TYPES[0] + '"){newurl+="linkType="+linkType+"&"}if (linkType=="' + VALID_LINK_TYPES[0] + '"){if (startFrom != "' + VALID_START_FROM[0] + '"){newurl+="startFrom="+startFrom+"&"}if (controls != "' + VALID_CONTROLS[0] + '"){newurl+="controls="+controls+"&"}}if (linkType=="Stream"){if (force_vod != "' + VALID_FORCE_VOD[0] + '"){newurl+="force_vod="+force_vod+"&"}}if (scores != "' + VALID_SCORES[0] + '"){newurl+="scores="+scores+"&"}if (scan_mode != "' + session.data.scan_mode + '"){newurl+="scan_mode="+scan_mode+"&"}if (content_protect != ""){newurl+="content_protect="+content_protect+"&"}window.location=newurl.substring(0,newurl.length-1)}' + "\n"
1370
1388
 
1371
1389
  // Ajax function for multiview and highlights
1372
1390
  body += 'function makeGETRequest(url, callback){var request=new XMLHttpRequest();request.onreadystatechange=function(){if (request.readyState==4 && request.status==200){callback(request.responseText)}};request.open("GET", url);request.send();}' + "\n"
@@ -1499,7 +1517,7 @@ app.get('/', async function(req, res) {
1499
1517
  } else {
1500
1518
  force_vod = VALID_FORCE_VOD[0]
1501
1519
  }
1502
- var thislink = '/' + link
1520
+ var thislink = http_root + '/' + link
1503
1521
 
1504
1522
  let blackouts = {}
1505
1523
 
@@ -1600,7 +1618,7 @@ app.get('/', async function(req, res) {
1600
1618
  compareEnd.setHours(compareEnd.getHours()+4)
1601
1619
  }
1602
1620
  compareEnd.setHours(compareEnd.getHours()+4)
1603
- body += '<tr><td><span class="tooltip">' + compareStart.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + compareEnd.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '<span class="tooltiptext">The game changer stream will automatically switch between the highest leverage active live non-blackout games, and should be available whenever there are such games available. Does not support adaptive bitrate switching, will default to best resolution if not specified.</span></span></td><td>'
1621
+ 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>'
1604
1622
  if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1605
1623
  let streamURL = server + '/gamechanger.m3u8'
1606
1624
  let multiviewquerystring = streamURL + '?resolution=' + DEFAULT_MULTIVIEW_RESOLUTION + content_protect_b
@@ -2048,7 +2066,7 @@ app.get('/', async function(req, res) {
2048
2066
  }
2049
2067
 
2050
2068
  if ( mediaType == VALID_MEDIA_TYPES[0] ) {
2051
- 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. 720p60 is the best quality. 540p 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>: '
2069
+ 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. 1080p60 or 720p60 is the best quality. 540p 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>: '
2052
2070
  for (var i = 0; i < VALID_RESOLUTIONS.length; i++) {
2053
2071
  body += '<button '
2054
2072
  if ( resolution == VALID_RESOLUTIONS[i] ) body += 'class="default" '
@@ -2105,7 +2123,7 @@ app.get('/', async function(req, res) {
2105
2123
  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"
2106
2124
  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"
2107
2125
  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"/>'
2108
- body += '<hr>Watch: <a href="/embed.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Embed</a> | <a href="' + multiview_server + multiview_url_path + content_protect_b + '">Stream</a> | <a href="/chromecast.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Chromecast</a> | <a href="/advanced.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Advanced</a> | <a href="/download.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '&filename=' + gameDate + ' Multiview">Download</a><br/><span class="tinytext">Kodi STRM files: <a href="/kodi.strm?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Matrix/19+</a> (<a href="/kodi.strm?version=18&src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Leia/18</a>)</span>'
2126
+ body += '<hr>Watch: <a href="' + http_root + '/embed.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Embed</a> | <a href="' + multiview_server + multiview_url_path + content_protect_b + '">Stream</a> | <a href="' + http_root + '/chromecast.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Chromecast</a> | <a href="' + http_root + '/advanced.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Advanced</a> | <a href="' + http_root + '/download.html?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '&filename=' + gameDate + ' Multiview">Download</a><br/><span class="tinytext">Kodi STRM files: <a href="' + http_root + '/kodi.strm?src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Matrix/19+</a> (<a href="' + http_root + '/kodi.strm?version=18&src=' + encodeURIComponent(multiview_server + multiview_url_path) + content_protect_b + '">Leia/18</a>)</span>'
2109
2127
  body += '</td></tr></table><br/>' + "\n"
2110
2128
  }
2111
2129
 
@@ -2135,42 +2153,42 @@ app.get('/', async function(req, res) {
2135
2153
  resolution = 'best'
2136
2154
  }
2137
2155
 
2138
- body += '<p><span class="tooltip">All<span class="tooltiptext">Will include all live MLB broadcasts (all games plus MLB Network, Big Inning, Game Changer, and Multiview). 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="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + content_protect_b + '">guide.xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + content_protect_b + '">calendar.ics</a></p>' + "\n"
2156
+ body += '<p><span class="tooltip">All<span class="tooltiptext">Will include all live MLB broadcasts (all games plus MLB Network, Big Inning, Game Changer, and Multiview). 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"
2139
2157
 
2140
2158
  let include_teams = 'ath,national'
2141
2159
  if ( session.credentials.fav_teams.length > 0 ) {
2142
2160
  include_teams = session.credentials.fav_teams.toString()
2143
2161
  }
2144
- 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="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=' + include_teams + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + content_protect_b + '">guide.xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + content_protect_b + '">calendar.ics</a></p>' + "\n"
2162
+ 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"
2145
2163
 
2146
- 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="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=' + include_teams + '&includeBlackouts=true' + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeBlackouts=true' + content_protect_b + '">guide.xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeBlackouts=true' + content_protect_b + '">calendar.ics</a></p>' + "\n"
2164
+ 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"
2147
2165
 
2148
2166
  let exclude_teams = 'ath,national'
2149
- body += '<p><span class="tooltip">Exclude a team + national<span class="tooltiptext">Excluding a team (MLB only, by abbreviation, in a comma-separated list if more than 1) will exclude every game involving that team. National refers to <a href="https://www.mlb.com/live-stream-games/national-blackout">USA national TV broadcasts</a>.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&excludeTeams=' + exclude_teams + content_protect_b + '">m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">ics</a></p>' + "\n"
2167
+ body += '<p><span class="tooltip">Exclude a team + national<span class="tooltiptext">Excluding a team (MLB only, by abbreviation, in a comma-separated list if more than 1) will exclude every game involving that team. National refers to <a href="https://www.mlb.com/live-stream-games/national-blackout">USA national TV broadcasts</a>.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&excludeTeams=' + exclude_teams + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&excludeTeams=' + exclude_teams + content_protect_b + '">ics</a></p>' + "\n"
2150
2168
 
2151
- body += '<p><span class="tooltip">Include (or exclude) LIDOM<span class="tooltiptext">Dominican Winter League, aka Liga de Beisbol Dominicano. Live stream only, does not support starting from the beginning or certain innings, skip options, etc.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=lidom' + content_protect_b + '">m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=lidom' + content_protect_b + '">xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeTeams=lidom' + content_protect_b + '">ics</a></p>' + "\n"
2169
+ body += '<p><span class="tooltip">Include (or exclude) LIDOM<span class="tooltiptext">Dominican Winter League, aka Liga de Beisbol Dominicano. Live stream only, does not support starting from the beginning or certain innings, skip options, etc.</span></span>: <a href="' + http_root + '/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=lidom' + content_protect_b + '">m3u</a> and <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=lidom' + content_protect_b + '">xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=lidom' + content_protect_b + '">ics</a></p>' + "\n"
2152
2170
 
2153
- 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://www.mlb.com/news/mlb-network-launches-direct-to-consumer-streaming-option">See here for more information</a>.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=mlbn' + content_protect_b + '">m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=mlbn' + content_protect_b + '">xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeTeams=mlb' + content_protect_b + '">ics</a></p>' + "\n"
2171
+ 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://www.mlb.com/news/mlb-network-launches-direct-to-consumer-streaming-option">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> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=mlb' + content_protect_b + '">ics</a></p>' + "\n"
2154
2172
 
2155
- 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="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=biginning' + content_protect_b + '">m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=biginning' + content_protect_b + '">xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeTeams=biginning' + content_protect_b + '">ics</a></p>' + "\n"
2173
+ 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"
2156
2174
 
2157
2175
  let gamechanger_resolution = resolution
2158
2176
  if ( gamechanger_resolution == VALID_RESOLUTIONS[0] ) {
2159
2177
  gamechanger_resolution = 'best'
2160
2178
  }
2161
- 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="/channels.m3u?mediaType=' + mediaType + '&resolution=' + gamechanger_resolution + '&includeTeams=gamechanger' + content_protect_b + '">m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=gamechanger' + content_protect_b + '">xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeTeams=gamechanger' + content_protect_b + '">ics</a></p>' + "\n"
2179
+ 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"
2162
2180
 
2163
- 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="/channels.m3u?mediaType=' + mediaType + '&includeTeams=multiview' + content_protect_b + '">m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=multiview' + content_protect_b + '">xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeTeams=multiview' + content_protect_b + '">ics</a></p>' + "\n"
2181
+ 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"
2164
2182
 
2165
2183
  if ( argv.free ) {
2166
- 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="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=free' + content_protect_b + '">m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=free' + content_protect_b + '">xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeTeams=free' + content_protect_b + '">ics</a></p>' + "\n"
2184
+ 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"
2167
2185
  }
2168
2186
 
2169
- 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="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeOrgs=ath,atl' + content_protect_b + '">m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeOrgs=ath' + content_protect_b + '">xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeOrgs=ath' + content_protect_b + '">ics</a></p>' + "\n"
2187
+ 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"
2170
2188
 
2171
- 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="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeLevels=a%2B,aaa' + content_protect_b + '">m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeLevels=a%2B,aaa' + content_protect_b + '">xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeLevels=a%2B,aaa' + content_protect_b + '">ics</a></p>' + "\n"
2189
+ 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"
2172
2190
 
2173
- body += '<p><span class="tooltip">Include teams in titles<span class="tooltiptext">An optional parameter added to the URL will include team names in the ICS/XML titles.</span></span>: <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">guide.xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">calendar.ics</a></p>' + "\n"
2191
+ body += '<p><span class="tooltip">Include teams in titles<span class="tooltiptext">An optional parameter added to the URL will include team names in the ICS/XML titles.</span></span>: <a href="' + http_root + '/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">guide.xml</a> and <a href="' + http_root + '/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">calendar.ics</a></p>' + "\n"
2174
2192
 
2175
2193
  body += '</td></tr></table><br/>' + "\n"
2176
2194
 
@@ -2207,7 +2225,7 @@ app.get('/', async function(req, res) {
2207
2225
  for (var i=0; i<examples.length; i++) {
2208
2226
  body += '&bull; ' + examples[i][0] + ': '
2209
2227
  for (var j=0; j<example_types.length; j++) {
2210
- body += '<a href="/' + example_types[j][0] + examples[i][1]
2228
+ body += '<a href="' + http_root + '/' + example_types[j][0] + examples[i][1]
2211
2229
  body += content_protect_b
2212
2230
  body += '">' + example_types[j][1] + '</a>'
2213
2231
  if ( j < (example_types.length-1) ) {
@@ -2225,17 +2243,17 @@ app.get('/', async function(req, res) {
2225
2243
  let gamechanger_types = ['in', 'ex']
2226
2244
  for (var i=0; i<gamechanger_types.length; i++) {
2227
2245
  let example_streamURL = gamechanger_streamURL + '&' + gamechanger_types[i] + 'cludeTeams=ath'
2228
- body += '&bull; ' + gamechanger_types[i] + 'clude: <a href="/embed.html?src=' + encodeURIComponent(example_streamURL) + '&startFrom=' + VALID_START_FROM[1] + content_protect_b + '">Embed</a> | <a href="' + example_streamURL + '">Stream</a> | <a href="/chromecast.html?src=' + encodeURIComponent(example_streamURL) + content_protect_b + '">Chromecast</a> | <a href="/advanced.html?src=' + encodeURIComponent(example_streamURL) + content_protect_b + '">Advanced</a> | <a href="/kodi.strm?src=' + encodeURIComponent(example_streamURL) + content_protect_b + '">Kodi</a><br/>' + "\n"
2246
+ 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"
2229
2247
  }
2230
2248
 
2231
2249
  body += '</p></td></tr></table><br/>' + "\n"
2232
2250
 
2233
- body += '<p><span class="tooltip">Sample video<span class="tooltiptext">A sample stream. Useful for testing and troubleshooting.</span></span>: <a href="/embed.html' + content_protect_a + '">Embed</a> | <a href="/stream.m3u8' + content_protect_a + '">Stream</a> | <a href="/chromecast.html' + content_protect_a + '">Chromecast</a> | <a href="/advanced.html' + content_protect_a + '">Advanced</a></p>' + "\n"
2251
+ 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"
2234
2252
 
2235
2253
  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"
2236
2254
 
2237
2255
  // Print version
2238
- body += '<p class="tinytext">Version ' + version + ' (<a href="/clearcache">clear session and cache</a>)</p>' + "\n"
2256
+ body += '<p class="tinytext">Version ' + version + ' (<a href="' + http_root + '/clearcache">clear session and cache</a>)</p>' + "\n"
2239
2257
 
2240
2258
  // Datepicker functions
2241
2259
  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"
@@ -2286,7 +2304,7 @@ app.get('/embed.html', async function(req, res) {
2286
2304
  controls = req.query.controls
2287
2305
  }
2288
2306
 
2289
- let video_url = '/stream.m3u8'
2307
+ let video_url = http_root + '/stream.m3u8'
2290
2308
  if ( req.query.src ) {
2291
2309
  video_url = req.query.src
2292
2310
  } else {
@@ -2303,7 +2321,7 @@ app.get('/embed.html', async function(req, res) {
2303
2321
  }
2304
2322
 
2305
2323
  // Adapted from https://hls-js.netlify.app/demo/basic-usage.html and https://hls-js-dev.netlify.app/demo
2306
- var body = '<html><head><meta charset="UTF-8"><meta http-equiv="Content-type" content="text/html;charset=UTF-8"><title>' + appname + ' player</title><link rel="icon" href="favicon.svg' + content_protect + '"><style type="text/css">input[type=text],input[type=button]{-webkit-appearance:none;-webkit-border-radius:0}body{background-color:black;color:lightgrey;font-family:Arial,Helvetica,sans-serif}video{width:100% !important;height:auto !important;max-width:1280px}input[type=number]::-webkit-inner-spin-button{opacity:1}button{color:lightgray;background-color:black}button.default{color:black;background-color:lightgray}</style><script>function goBack(){var prevPage=window.location.href;window.history.go(-1);setTimeout(function(){if(window.location.href==prevPage){window.location.href="/' + content_protect + '"}}, 500)}function toggleAudio(x){var elements=document.getElementsByClassName("audioButton");for(var i=0;i<elements.length;i++){elements[i].className="audioButton"}document.getElementById("audioButton"+x).className+=" default";hls.audioTrack=x}function changeTime(x){video.currentTime+=x}function changeRate(x){let newRate=Math.round((Number(document.getElementById("playback_rate").value)+x)*10)/10;if((newRate<=document.getElementById("playback_rate").max) && (newRate>=document.getElementById("playback_rate").min)){document.getElementById("playback_rate").value=newRate.toFixed(1);video.defaultPlaybackRate=video.playbackRate=document.getElementById("playback_rate").value}}function myKeyPress(e){if(e.key=="ArrowRight"){changeTime(10)}else if(e.key=="ArrowLeft"){changeTime(-10)}else if(e.key=="ArrowUp"){changeRate(0.1)}else if(e.key=="ArrowDown"){changeRate(-0.1)}}</script></head><body onkeydown="myKeyPress(event)"><script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script><video id="video"'
2324
+ var body = '<html><head><meta charset="UTF-8"><meta http-equiv="Content-type" content="text/html;charset=UTF-8"><title>' + appname + ' player</title><link rel="icon" href="favicon.svg' + content_protect + '"><style type="text/css">input[type=text],input[type=button]{-webkit-appearance:none;-webkit-border-radius:0}body{background-color:black;color:lightgrey;font-family:Arial,Helvetica,sans-serif}video{width:100% !important;height:auto !important;max-width:1280px}input[type=number]::-webkit-inner-spin-button{opacity:1}button{color:lightgray;background-color:black}button.default{color:black;background-color:lightgray}</style><script>function goBack(){var prevPage=window.location.href;window.history.go(-1);setTimeout(function(){if(window.location.href==prevPage){window.location.href="' + http_root + '/' + content_protect + '"}}, 500)}function toggleAudio(x){var elements=document.getElementsByClassName("audioButton");for(var i=0;i<elements.length;i++){elements[i].className="audioButton"}document.getElementById("audioButton"+x).className+=" default";hls.audioTrack=x}function changeTime(x){video.currentTime+=x}function changeRate(x){let newRate=Math.round((Number(document.getElementById("playback_rate").value)+x)*10)/10;if((newRate<=document.getElementById("playback_rate").max) && (newRate>=document.getElementById("playback_rate").min)){document.getElementById("playback_rate").value=newRate.toFixed(1);video.defaultPlaybackRate=video.playbackRate=document.getElementById("playback_rate").value}}function myKeyPress(e){if(e.key=="ArrowRight"){changeTime(10)}else if(e.key=="ArrowLeft"){changeTime(-10)}else if(e.key=="ArrowUp"){changeRate(0.1)}else if(e.key=="ArrowDown"){changeRate(-0.1)}}</script></head><body onkeydown="myKeyPress(event)"><script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script><video id="video"'
2307
2325
  if ( controls == VALID_CONTROLS[0] ) {
2308
2326
  body += ' controls'
2309
2327
  }
@@ -2327,7 +2345,7 @@ app.get('/advanced.html', async function(req, res) {
2327
2345
 
2328
2346
  session.requestlog('advanced.html', req)
2329
2347
 
2330
- let server = 'http://' + req.headers.host
2348
+ let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
2331
2349
 
2332
2350
  let video_url = '/stream.m3u8'
2333
2351
  if ( req.query.src ) {
@@ -2350,7 +2368,7 @@ app.get('/chromecast.html', async function(req, res) {
2350
2368
 
2351
2369
  session.requestlog('chromecast.html', req)
2352
2370
 
2353
- let server = 'http://' + req.headers.host
2371
+ let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
2354
2372
 
2355
2373
  let video_url = '/stream.m3u8'
2356
2374
  if ( req.query.src ) {
@@ -2387,7 +2405,7 @@ app.get('/channels.m3u', async function(req, res) {
2387
2405
  excludeTeams = req.query.excludeTeams.toUpperCase().split(',')
2388
2406
  }
2389
2407
 
2390
- let server = 'http://' + req.headers.host
2408
+ let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
2391
2409
 
2392
2410
  let resolution = 'best'
2393
2411
  if ( req.query.resolution ) {
@@ -2466,7 +2484,7 @@ app.get('/calendar.ics', async function(req, res) {
2466
2484
  includeTeamsInTitles = req.query.includeTeamsInTitles
2467
2485
  }
2468
2486
 
2469
- let server = 'http://' + req.headers.host
2487
+ let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
2470
2488
 
2471
2489
  var body = await session.getTVData('calendar', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, includeTeamsInTitles)
2472
2490
 
@@ -2519,7 +2537,7 @@ app.get('/guide.xml', async function(req, res) {
2519
2537
  includeTeamsInTitles = req.query.includeTeamsInTitles
2520
2538
  }
2521
2539
 
2522
- let server = 'http://' + req.headers.host
2540
+ let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
2523
2541
 
2524
2542
  var body = await session.getTVData('guide', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, includeTeamsInTitles)
2525
2543
 
@@ -2893,7 +2911,7 @@ app.get('/kodi.strm', async function(req, res) {
2893
2911
  try {
2894
2912
  session.requestlog('kodi.strm', req)
2895
2913
 
2896
- let server = 'http://' + req.headers.host
2914
+ let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
2897
2915
 
2898
2916
  let video_url = '/stream.m3u8'
2899
2917
  let file_name = 'kodi'
@@ -2943,7 +2961,7 @@ app.get('/download.html', async function(req, res) {
2943
2961
  session.debuglog('force alternate audio', req)
2944
2962
  }
2945
2963
 
2946
- let server = 'http://' + req.headers.host
2964
+ let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
2947
2965
 
2948
2966
  let video_url = '/stream.m3u8'
2949
2967
  if ( req.query.src ) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2025.02.16",
3
+ "version": "2025.02.21",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",