mlbserver 2026.3.15 → 2026.4.1-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.
- package/index.js +212 -49
- package/package.json +1 -1
- package/session.js +114 -84
package/index.js
CHANGED
|
@@ -262,6 +262,7 @@ app.get('/clearcache', async function(req, res) {
|
|
|
262
262
|
session.log('Clearing session...')
|
|
263
263
|
session.clear_session_data()
|
|
264
264
|
session = new sessionClass(argv)
|
|
265
|
+
session.setPorts(port, multiview_port)
|
|
265
266
|
|
|
266
267
|
let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
|
|
267
268
|
res.redirect(server)
|
|
@@ -431,7 +432,7 @@ var getKey = function(url, headers, cb) {
|
|
|
431
432
|
requestRetry(url, headers, function(err, response) {
|
|
432
433
|
if (err) return cb(err)
|
|
433
434
|
let key = response.body
|
|
434
|
-
session.debuglog('key returned ' + key)
|
|
435
|
+
session.debuglog('key returned ' + Buffer.from(key, 'binary').toString('base64'))
|
|
435
436
|
session.temp_cache.prevKeys[url] = key
|
|
436
437
|
cb(null, key)
|
|
437
438
|
})
|
|
@@ -1262,15 +1263,28 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
|
|
|
1262
1263
|
let new_segments_complete = false
|
|
1263
1264
|
let segment_count = 0
|
|
1264
1265
|
for (var i=(body.length-1); i>=0; i--) {
|
|
1265
|
-
if ( body[i].startsWith('#
|
|
1266
|
-
let
|
|
1267
|
-
|
|
1268
|
-
|
|
1266
|
+
if ( body[i].startsWith('#EXT-X-KEY') ) {
|
|
1267
|
+
let key = url.resolve(u, body[i].match('URI="([^"]+)"')[1])
|
|
1268
|
+
let iv = body[i].match('IV=0x(.*)$')[1]
|
|
1269
|
+
let ts
|
|
1270
|
+
let extinf
|
|
1271
|
+
for (var j=1; j<=4; j++) {
|
|
1272
|
+
if ( body[i+j] ) {
|
|
1273
|
+
if ( !extinf && body[i+j].startsWith('#EXTINF') ) {
|
|
1274
|
+
extinf = body[i+j]
|
|
1275
|
+
} else if ( !ts && !body[i+j].startsWith('#') ) {
|
|
1276
|
+
ts = url.resolve(u, body[i+j])
|
|
1277
|
+
}
|
|
1278
|
+
if ( extinf && ts ) break;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if ( key && iv && extinf && ts && !new_segments_complete ) {
|
|
1282
|
+
session.debuglog(game_changer_title + 'found segment ' + ts)
|
|
1269
1283
|
if ( discontinuity ) {
|
|
1270
1284
|
session.debuglog(game_changer_title + 'only getting newest segment after stream change')
|
|
1271
|
-
new_segments.unshift({'extinf':
|
|
1285
|
+
new_segments.unshift({'key':key, 'iv':iv, 'extinf':extinf, 'ts':ts, 'streamURLToken':streamURLToken})
|
|
1272
1286
|
new_segments_complete = true
|
|
1273
|
-
} else if ( !discontinuity && (session.temp_cache.gamechanger[id].segments.length > 0) && (
|
|
1287
|
+
} else if ( !discontinuity && (session.temp_cache.gamechanger[id].segments.length > 0) && (ts == session.temp_cache.gamechanger[id].segments[session.temp_cache.gamechanger[id].segments.length-1].ts) ) {
|
|
1274
1288
|
session.debuglog(game_changer_title + 'found previous last segment')
|
|
1275
1289
|
new_segments_complete = true
|
|
1276
1290
|
} else if ( segment_count == GAMECHANGER_LIST_SIZE ) {
|
|
@@ -1281,7 +1295,7 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
|
|
|
1281
1295
|
}
|
|
1282
1296
|
new_segments_complete = true
|
|
1283
1297
|
} else {
|
|
1284
|
-
new_segments.unshift({'extinf':
|
|
1298
|
+
new_segments.unshift({'key':key, 'iv':iv, 'extinf':extinf, 'ts':ts, 'streamURLToken':streamURLToken})
|
|
1285
1299
|
}
|
|
1286
1300
|
}
|
|
1287
1301
|
segment_count++
|
|
@@ -1314,7 +1328,7 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
|
|
|
1314
1328
|
if ( session.temp_cache.gamechanger[id].segments[i].discontinuity ) {
|
|
1315
1329
|
session.temp_cache.gamechanger[id].playlist[resolution] += '#EXT-X-DISCONTINUITY' + '\n'
|
|
1316
1330
|
}
|
|
1317
|
-
session.temp_cache.gamechanger[id].playlist[resolution] += session.temp_cache.gamechanger[id].segments[i].extinf + '\n' + http_root + '/segment.ts?url=' + encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].ts) + '&streamURLToken='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].streamURLToken) + content_protect + '\n'
|
|
1331
|
+
session.temp_cache.gamechanger[id].playlist[resolution] += session.temp_cache.gamechanger[id].segments[i].extinf + '\n' + http_root + '/segment.ts?url=' + encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].ts) + '&streamURLToken='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].streamURLToken) + '&key='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].key) + '&iv='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].iv) + content_protect + '\n'
|
|
1318
1332
|
}
|
|
1319
1333
|
|
|
1320
1334
|
session.debuglog(game_changer_title + 'playlist ' + session.temp_cache.gamechanger[id].playlist[resolution])
|
|
@@ -1529,8 +1543,7 @@ app.get('/', async function(req, res) {
|
|
|
1529
1543
|
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}'
|
|
1530
1544
|
|
|
1531
1545
|
// Highlights CSS
|
|
1532
|
-
|
|
1533
|
-
body += '.modal{display:none;position:fixed;z-index:1;padding-top:100px;left:0;top:0;width:100%;height:100%;overflow:auto;-webkit-overflow-scrolling:touch;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}.modal-content{background-color:#fefefe;margin:auto;padding:10px;border:1px solid #888;width:360px;color:black}#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;}'
|
|
1546
|
+
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;}'
|
|
1534
1547
|
|
|
1535
1548
|
// Tooltip CSS
|
|
1536
1549
|
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;}'
|
|
@@ -1816,37 +1829,38 @@ app.get('/', async function(req, res) {
|
|
|
1816
1829
|
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') ) {
|
|
1817
1830
|
// Scraped Big Inning schedule
|
|
1818
1831
|
big_inning = await session.getBigInningSchedule(gameDate)
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1832
|
+
}
|
|
1833
|
+
if ( big_inning ) {
|
|
1834
|
+
for (var i = 0; i < big_inning.length; i++) {
|
|
1835
|
+
if ( big_inning[i].start ) {
|
|
1836
|
+
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>'
|
|
1837
|
+
let compareStart = new Date(big_inning[i].start)
|
|
1838
|
+
compareStart.setMinutes(compareStart.getMinutes()-10)
|
|
1839
|
+
let compareEnd = new Date(big_inning[i].end)
|
|
1840
|
+
compareEnd.setHours(compareEnd.getHours()+1)
|
|
1841
|
+
if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
|
|
1842
|
+
let querystring = '?event=biginning'
|
|
1843
|
+
let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
|
|
1844
|
+
if ( linkType == VALID_LINK_TYPES[0] ) {
|
|
1845
|
+
if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
|
|
1846
|
+
if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
|
|
1847
|
+
}
|
|
1848
|
+
if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
|
|
1849
|
+
if ( linkType == VALID_LINK_TYPES[1] ) {
|
|
1850
|
+
if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
|
|
1851
|
+
} else if ( linkType == VALID_LINK_TYPES[4] ) {
|
|
1852
|
+
querystring += '&filename=' + gameDate + ' Big Inning'
|
|
1853
|
+
}
|
|
1854
|
+
querystring += content_protect_b
|
|
1855
|
+
multiviewquerystring += content_protect_b
|
|
1856
|
+
body += '<a href="' + thislink + querystring + '">Big Inning</a>'
|
|
1857
|
+
body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
|
|
1858
|
+
} else {
|
|
1859
|
+
body += 'Big Inning'
|
|
1860
|
+
}
|
|
1861
|
+
body += '</td></tr>' + "\n"
|
|
1841
1862
|
}
|
|
1842
|
-
querystring += content_protect_b
|
|
1843
|
-
multiviewquerystring += content_protect_b
|
|
1844
|
-
body += '<a href="' + thislink + querystring + '">Big Inning</a>'
|
|
1845
|
-
body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
|
|
1846
|
-
} else {
|
|
1847
|
-
body += 'Big Inning'
|
|
1848
1863
|
}
|
|
1849
|
-
body += '</td></tr>' + "\n"
|
|
1850
1864
|
}
|
|
1851
1865
|
|
|
1852
1866
|
// Game Changer and Stream Finder
|
|
@@ -3307,11 +3321,13 @@ function start_multiview_stream(streams, sync, dvr, faster, reencode, park_audio
|
|
|
3307
3321
|
//let audio_input = audio_present[i] + ':a:m:language:en?'
|
|
3308
3322
|
let audio_input = audio_present[i] + ':a:'
|
|
3309
3323
|
let video_url = streams[audio_present[i]]
|
|
3310
|
-
|
|
3324
|
+
// disabled code below because we can now assume
|
|
3325
|
+
// the first audio track is the one we want
|
|
3326
|
+
//if ( !video_url || video_url.includes('audio_track=English') || !video_url.includes('audio_track=') ) {
|
|
3311
3327
|
audio_input += '0'
|
|
3312
|
-
} else {
|
|
3328
|
+
/*} else {
|
|
3313
3329
|
audio_input += '1'
|
|
3314
|
-
}
|
|
3330
|
+
}*/
|
|
3315
3331
|
let filter = ''
|
|
3316
3332
|
// Optionally apply sync adjustments
|
|
3317
3333
|
if ( sync[audio_present[i]] ) {
|
|
@@ -3518,11 +3534,13 @@ app.get('/download.ts', async function(req, res) {
|
|
|
3518
3534
|
if ( ! (await protect(req, res)) ) return
|
|
3519
3535
|
|
|
3520
3536
|
try {
|
|
3537
|
+
let ffmpeg_timeout = 432000
|
|
3521
3538
|
// we'll know it's an actual download request if it include a filename parameter
|
|
3522
3539
|
if ( req.query.filename ) {
|
|
3523
3540
|
session.requestlog('download.ts', req)
|
|
3524
3541
|
} else {
|
|
3525
3542
|
session.debuglog('force alternate audio', req)
|
|
3543
|
+
ffmpeg_timeout = 20
|
|
3526
3544
|
}
|
|
3527
3545
|
|
|
3528
3546
|
let server = 'http://127.0.0.1:' + session.data.port + http_root
|
|
@@ -3548,7 +3566,7 @@ app.get('/download.ts', async function(req, res) {
|
|
|
3548
3566
|
}
|
|
3549
3567
|
}
|
|
3550
3568
|
|
|
3551
|
-
ffmpeg_command = ffmpeg({ timeout:
|
|
3569
|
+
ffmpeg_command = ffmpeg({ timeout: ffmpeg_timeout })
|
|
3552
3570
|
|
|
3553
3571
|
// Set input stream and minimize ffmpeg startup latency
|
|
3554
3572
|
ffmpeg_command.input(video_url)
|
|
@@ -3606,15 +3624,21 @@ app.get('/download.ts', async function(req, res) {
|
|
|
3606
3624
|
ffmpeg_command.addOutputOption('-f', 'mpegts')
|
|
3607
3625
|
.output(res)
|
|
3608
3626
|
.on('start', function(commandLine) {
|
|
3609
|
-
session.debuglog('download.ts command started')
|
|
3627
|
+
session.debuglog('download.ts command started for ' + video_url)
|
|
3610
3628
|
if ( argv.debug || argv.ffmpeg_logging ) {
|
|
3611
|
-
session.
|
|
3629
|
+
session.log('download.ts command: ' + commandLine)
|
|
3612
3630
|
}
|
|
3613
3631
|
})
|
|
3614
3632
|
.on('error', function(err, stdout, stderr) {
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3633
|
+
if (err.message.includes('timeout')) {
|
|
3634
|
+
session.debuglog('download.ts command timeout: ' + err.message)
|
|
3635
|
+
} else {
|
|
3636
|
+
session.debuglog('download.ts command error: ' + err.message)
|
|
3637
|
+
}
|
|
3638
|
+
if ( stdout ) session.debuglog(stdout)
|
|
3639
|
+
if ( stderr ) session.debuglog(stderr)
|
|
3640
|
+
ffmpeg_command.kill('SIGKILL')
|
|
3641
|
+
session.debuglog('killed ffmpeg process due to error processing ' + video_url)
|
|
3618
3642
|
})
|
|
3619
3643
|
.on('end', function() {
|
|
3620
3644
|
session.debuglog('download.ts command ended')
|
|
@@ -3782,3 +3806,142 @@ app.get('/comskip.txt', async function(req, res) {
|
|
|
3782
3806
|
res.end('comskip.txt request error, check log')
|
|
3783
3807
|
}
|
|
3784
3808
|
})
|
|
3809
|
+
|
|
3810
|
+
// Listen for embedded MPEGTS stream requests
|
|
3811
|
+
// embedded player not fully tested
|
|
3812
|
+
app.get('/mpegts.html', async function(req, res) {
|
|
3813
|
+
if ( ! (await protect(req, res)) ) return
|
|
3814
|
+
|
|
3815
|
+
try {
|
|
3816
|
+
let server = 'http://127.0.0.1:' + session.data.port + http_root
|
|
3817
|
+
|
|
3818
|
+
let video_url = '/stream.ts'
|
|
3819
|
+
if ( req.query.src ) {
|
|
3820
|
+
video_url = req.query.src
|
|
3821
|
+
} else {
|
|
3822
|
+
let urlArray = req.url.split('?')
|
|
3823
|
+
if ( (urlArray.length == 2) ) {
|
|
3824
|
+
video_url += '?' + urlArray[1]
|
|
3825
|
+
}
|
|
3826
|
+
video_url = server + video_url
|
|
3827
|
+
}
|
|
3828
|
+
session.debuglog('mpegts.html src : ' + video_url)
|
|
3829
|
+
|
|
3830
|
+
var body = '<html><script src="https://xqq.im/mpegts.js/dist/mpegts.js"></script><style type"text/css">body{background-color:black}video{width:100% !important;height:auto !important;max-width:1280px}</style><body><video id="videoElement" controls autoplay playsinline></video><script>if (mpegts.getFeatureList().mseLivePlayback) { var videoElement = document.getElementById("videoElement"); var player = mpegts.createPlayer({ type: "mpegts", isLive: true, url: "' + video_url + '" }); player.attachMediaElement(videoElement); player.load(); player.play(); }</script></body></html>'
|
|
3831
|
+
|
|
3832
|
+
res.end(body)
|
|
3833
|
+
} catch (e) {
|
|
3834
|
+
session.log('mpegts.html request error : ' + e.message)
|
|
3835
|
+
res.end('')
|
|
3836
|
+
}
|
|
3837
|
+
})
|
|
3838
|
+
|
|
3839
|
+
|
|
3840
|
+
// Listen for MPEGTS stream requests
|
|
3841
|
+
app.get('/stream.ts', async function(req, res) {
|
|
3842
|
+
if ( ! (await protect(req, res)) ) return
|
|
3843
|
+
|
|
3844
|
+
try {
|
|
3845
|
+
let server = 'http://127.0.0.1:' + session.data.port + http_root
|
|
3846
|
+
|
|
3847
|
+
let video_url = '/stream.m3u8'
|
|
3848
|
+
if ( req.query.src ) {
|
|
3849
|
+
video_url = req.query.src
|
|
3850
|
+
} else {
|
|
3851
|
+
let urlArray = req.url.split('?')
|
|
3852
|
+
if ( (urlArray.length == 2) ) {
|
|
3853
|
+
video_url += '?' + urlArray[1]
|
|
3854
|
+
}
|
|
3855
|
+
video_url = server + video_url
|
|
3856
|
+
}
|
|
3857
|
+
session.debuglog('stream.ts src : ' + video_url)
|
|
3858
|
+
|
|
3859
|
+
// force adaptive streams to just use a single video resolution/track
|
|
3860
|
+
if ( !video_url.includes('resolution=') ) {
|
|
3861
|
+
video_url += '&resolution=best'
|
|
3862
|
+
} else if ( video_url.includes('resolution=adaptive') ) {
|
|
3863
|
+
video_url = video_url.replace('resolution=adaptive', 'resolution=best')
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
// force streams to just use a single audio track, if they aren't already
|
|
3867
|
+
if ( !video_url.includes('audio_track=') ) {
|
|
3868
|
+
video_url += '&audio_track=English'
|
|
3869
|
+
} else if ( video_url.includes('audio_track=all') ) {
|
|
3870
|
+
video_url = video_url.replace('audio_track=all', 'audio_track=English')
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
ffmpeg_command = ffmpeg({ timeout: 432000 })
|
|
3874
|
+
|
|
3875
|
+
// Set input live stream and minimize ffmpeg startup latency
|
|
3876
|
+
ffmpeg_command.input(video_url)
|
|
3877
|
+
.addInputOption('-thread_queue_size', '4096')
|
|
3878
|
+
.addInputOption('-fflags', 'nobuffer')
|
|
3879
|
+
.addInputOption('-probesize', '1000000')
|
|
3880
|
+
.addInputOption('-analyzeduration', '0')
|
|
3881
|
+
|
|
3882
|
+
// We'll limit our processing to real-time
|
|
3883
|
+
ffmpeg_command.native()
|
|
3884
|
+
|
|
3885
|
+
let video_input = 0
|
|
3886
|
+
let audio_input = 0
|
|
3887
|
+
|
|
3888
|
+
// Adjust audio sync, if specified
|
|
3889
|
+
if ( req.query.sync ) {
|
|
3890
|
+
if ( req.query.sync > 0 ) {
|
|
3891
|
+
session.log('stream.ts delaying video by ' + req.query.sync + ' seconds')
|
|
3892
|
+
video_input = 1
|
|
3893
|
+
ffmpeg_command.addInputOption('-itsoffset', req.query.sync)
|
|
3894
|
+
} else {
|
|
3895
|
+
session.log('stream.ts delaying audio by ' + (req.query.sync * -1) + ' seconds')
|
|
3896
|
+
audio_input = 1
|
|
3897
|
+
ffmpeg_command.addInputOption('-itsoffset', (req.query.sync * -1))
|
|
3898
|
+
}
|
|
3899
|
+
ffmpeg_command.input(video_url)
|
|
3900
|
+
.addInputOption('-thread_queue_size', '4096')
|
|
3901
|
+
|
|
3902
|
+
// We'll limit our processing to real-time
|
|
3903
|
+
ffmpeg_command.native()
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
// video
|
|
3907
|
+
ffmpeg_command.addOutputOption('-map', video_input + ':v:0')
|
|
3908
|
+
.addOutputOption('-c:v', 'copy')
|
|
3909
|
+
|
|
3910
|
+
// audio
|
|
3911
|
+
ffmpeg_command.addOutputOption('-map', audio_input + ':a')
|
|
3912
|
+
.addOutputOption('-c:a', 'copy')
|
|
3913
|
+
|
|
3914
|
+
// output mpegts to response stream
|
|
3915
|
+
ffmpeg_command.addOutputOption('-f', 'mpegts')
|
|
3916
|
+
.output(res)
|
|
3917
|
+
.on('start', function(commandLine) {
|
|
3918
|
+
session.debuglog('stream.ts command started')
|
|
3919
|
+
if ( argv.debug || argv.ffmpeg_logging ) {
|
|
3920
|
+
session.log('stream.ts command: ' + commandLine)
|
|
3921
|
+
}
|
|
3922
|
+
})
|
|
3923
|
+
.on('error', function(err, stdout, stderr) {
|
|
3924
|
+
session.debuglog('stream.ts command stopped: ' + err.message)
|
|
3925
|
+
if ( stdout ) session.debuglog(stdout)
|
|
3926
|
+
if ( stderr ) session.debuglog(stderr)
|
|
3927
|
+
})
|
|
3928
|
+
.on('end', function() {
|
|
3929
|
+
session.debuglog('stream.ts command ended')
|
|
3930
|
+
})
|
|
3931
|
+
|
|
3932
|
+
if ( argv.ffmpeg_logging ) {
|
|
3933
|
+
session.log('ffmpeg output logging enabled')
|
|
3934
|
+
ffmpeg_command.on('stderr', function(stderrLine) {
|
|
3935
|
+
session.log(stderrLine);
|
|
3936
|
+
})
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
var headers = {'Content-Type': 'video/mp2t',"access-control-allow-origin":"*"}
|
|
3940
|
+
res.writeHead(200, headers)
|
|
3941
|
+
|
|
3942
|
+
ffmpeg_command.run()
|
|
3943
|
+
} catch (e) {
|
|
3944
|
+
session.log('stream.ts request error : ' + e.message)
|
|
3945
|
+
res.end('')
|
|
3946
|
+
}
|
|
3947
|
+
})
|
package/package.json
CHANGED
package/session.js
CHANGED
|
@@ -28,7 +28,7 @@ const LIDOM_TEAM_IDS = { 'AGU': '667', 'TOR': '668', 'EST': '669', 'GIG': '670',
|
|
|
28
28
|
|
|
29
29
|
const LMP_TEAM_IDS = { 'MXC': '673', 'JAL': '674', 'MOC': '675', 'HER': '677', 'CUL': '678', 'MAZ': '679', 'OBR': '680', 'GSV': '5482', 'NAY': '6483', 'TBC': '6484' }
|
|
30
30
|
|
|
31
|
-
const AFFILIATE_TEAM_IDS = {
|
|
31
|
+
const AFFILIATE_TEAM_IDS = {"ATH":"237,400,499,524","ATL":"431,432,478,6325","AZ":"419,516,2310,5368","BAL":"418,493,548,568","BOS":"414,428,533,546","CHC":"451,521,550,553","CIN":"416,450,459,498","CLE":"402,437,445,481","COL":"259,342,486,538","CWS":"247,487,494,580","DET":"106,512,570,582","HOU":"482,573,3712,5434","KC":"541,565,1350,3705","LAA":"460,526,559,561","LAD":"238,260,456,6482","MIA":"479,554,564,4124","MIL":"249,556,572,5015","MIN":"492,509,1960,3898","NYM":"453,505,507,552","NYY":"531,537,587,1956","PHI":"427,522,566,1410","PIT":"452,477,484,3390","SD":"103,510,584,4904","SEA":"401,403,529,574","SF":"105,461,476,3410","STL":"235,279,440,443","TB":"233,234,421,2498","TEX":"102,448,540,6324","TOR":"422,424,435,463","WSH":"426,436,534,547"}
|
|
32
32
|
|
|
33
33
|
// First is default level, last should be All (also used as default org)
|
|
34
34
|
const LEVELS = { 'MLB': '1', 'AAA': '11', 'AA': '12', 'A+': '13', 'A': '14', 'WINTER': '17', 'All': '1,11,12,13,14,17' }
|
|
@@ -2368,8 +2368,8 @@ class sessionClass {
|
|
|
2368
2368
|
stream = server + '/stream.m3u8?event=' + encodeURIComponent(cache_data.dates[i].games[j].teams['home'].team.shortName.toUpperCase())
|
|
2369
2369
|
}
|
|
2370
2370
|
stream += '&league_id=' + league_id
|
|
2371
|
+
stream += '&mediaType=' + streamMediaType
|
|
2371
2372
|
}
|
|
2372
|
-
stream += '&mediaType=' + streamMediaType
|
|
2373
2373
|
stream += '&level=' + encodeURIComponent(this.getLevelNameFromSportId(sportId))
|
|
2374
2374
|
stream += '&resolution=' + resolution
|
|
2375
2375
|
if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
|
|
@@ -2919,25 +2919,28 @@ class sessionClass {
|
|
|
2919
2919
|
let gameDate = cache_data.dates[i].date
|
|
2920
2920
|
if ( (gameDate >= today) && cache_data.dates[i].games && (cache_data.dates[i].games.length > 1) && cache_data.dates[i].games[0] && (cache_data.dates[i].games[0].seriesDescription == 'Regular Season') && this.cache.bigInningSchedule[gameDate] ) {
|
|
2921
2921
|
this.debuglog('getTVData Big Inning active for date ' + cache_data.dates[i].date)
|
|
2922
|
-
// Scraped Big Inning schedule
|
|
2923
|
-
let start = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].start))
|
|
2924
|
-
let stop = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].end))
|
|
2925
|
-
|
|
2926
|
-
// Big Inning calendar ICS
|
|
2927
|
-
let prefix = 'Watch'
|
|
2928
|
-
let location = server + '/embed.html?event=biginning&mediaType=Video&resolution=' + resolution
|
|
2929
|
-
if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
|
|
2930
|
-
calendar += await this.generate_ics_event(prefix, new Date(this.cache.bigInningSchedule[gameDate].start), new Date(this.cache.bigInningSchedule[gameDate].end), title, description, location)
|
|
2931
2922
|
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2923
|
+
for (var j = 0; j < this.cache.bigInningSchedule[gameDate].length; j++) {
|
|
2924
|
+
// Scraped Big Inning schedule
|
|
2925
|
+
let start = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate][j].start))
|
|
2926
|
+
let stop = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate][j].end))
|
|
2927
|
+
|
|
2928
|
+
// Big Inning calendar ICS
|
|
2929
|
+
let prefix = 'Watch'
|
|
2930
|
+
let location = server + '/embed.html?event=biginning&mediaType=Video&resolution=' + resolution
|
|
2931
|
+
if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
|
|
2932
|
+
calendar += await this.generate_ics_event(prefix, new Date(this.cache.bigInningSchedule[gameDate][j].start), new Date(this.cache.bigInningSchedule[gameDate][j].end), title, description, location)
|
|
2933
|
+
|
|
2934
|
+
// Off Air if necessary
|
|
2935
|
+
let off_air_event = await this.generate_off_air_event(offAir, channelid, gameDate, channels[channelid].stop, this.cache.bigInningSchedule[gameDate][j].start, title)
|
|
2936
|
+
if ( off_air_event ) {
|
|
2937
|
+
programs += off_air_event
|
|
2938
|
+
channels[channelid].stop = stop
|
|
2939
|
+
}
|
|
2938
2940
|
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
+
// Big Inning guide XML
|
|
2942
|
+
programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(new Date(this.cache.bigInningSchedule[gameDate][j].start)))
|
|
2943
|
+
}
|
|
2941
2944
|
}
|
|
2942
2945
|
this.debuglog('getTVData completed Big Inning for date ' + cache_data.dates[i].date)
|
|
2943
2946
|
}
|
|
@@ -3319,6 +3322,7 @@ class sessionClass {
|
|
|
3319
3322
|
async getSkipMarkers(gamePk, skip_type, start_inning, start_inning_half, streamURL, streamURLToken, skip_adjust, broadcast_start_timestamp=false) {
|
|
3320
3323
|
try {
|
|
3321
3324
|
this.debuglog('getSkipMarkers')
|
|
3325
|
+
let variantPlaylist;
|
|
3322
3326
|
|
|
3323
3327
|
if ( skip_adjust != 0 ) this.log('manual adjustment of ' + skip_adjust + ' seconds being applied')
|
|
3324
3328
|
|
|
@@ -3342,7 +3346,7 @@ class sessionClass {
|
|
|
3342
3346
|
|
|
3343
3347
|
// Get the broadcast start time first, if necessary -- event times will be relative to this
|
|
3344
3348
|
if ( !broadcast_start_timestamp ) {
|
|
3345
|
-
|
|
3349
|
+
variantPlaylist = await this.getVariantPlaylist(streamURL, streamURLToken)
|
|
3346
3350
|
broadcast_start_timestamp = await this.getBroadcastStart(variantPlaylist)
|
|
3347
3351
|
}
|
|
3348
3352
|
|
|
@@ -3488,6 +3492,10 @@ class sessionClass {
|
|
|
3488
3492
|
// if skipping commercials, look at the variant playlist to detect insertions
|
|
3489
3493
|
if ( skip_type == 4 ) {
|
|
3490
3494
|
this.debuglog('detecting commercial breaks')
|
|
3495
|
+
if (!variantPlaylist) {
|
|
3496
|
+
this.debuglog('variantPlaylist missing, fetching...')
|
|
3497
|
+
variantPlaylist = await this.getVariantPlaylist(streamURL, streamURLToken)
|
|
3498
|
+
}
|
|
3491
3499
|
let body = variantPlaylist
|
|
3492
3500
|
let break_active = false
|
|
3493
3501
|
let break_end = 0
|
|
@@ -3534,54 +3542,54 @@ class sessionClass {
|
|
|
3534
3542
|
// temporarily disable Big Inning schedule checking until a new source URL is available
|
|
3535
3543
|
/*this.cache.bigInningSchedule = {}
|
|
3536
3544
|
return*/
|
|
3537
|
-
|
|
3545
|
+
|
|
3546
|
+
// reset for new format as of 2026-04-01
|
|
3547
|
+
try {
|
|
3548
|
+
if ( !this.cache.bigInningSchedule[Object.keys(this.cache.bigInningSchedule)[0]].length ) {
|
|
3549
|
+
this.log('getBigInningSchedule cache reset')
|
|
3550
|
+
delete this.cache.bigInningScheduleCacheExpiry
|
|
3551
|
+
delete this.cache.bigInningSchedule
|
|
3552
|
+
}
|
|
3553
|
+
} catch (e) {
|
|
3554
|
+
//this.debuglog('getBigInningSchedule cache reset error : ' + e.message)
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3538
3557
|
let currentDate = new Date()
|
|
3539
3558
|
if ( !this.cache || !this.cache.bigInningScheduleCacheExpiry || (currentDate > new Date(this.cache.bigInningScheduleCacheExpiry)) ) {
|
|
3540
3559
|
if ( !this.cache.bigInningSchedule ) this.cache.bigInningSchedule = {}
|
|
3541
3560
|
let reqObj = {
|
|
3542
|
-
url: 'https://
|
|
3561
|
+
url: 'https://www.fubo.tv/welcome/channel/mlb-big-inning',
|
|
3543
3562
|
headers: {
|
|
3544
|
-
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9
|
|
3545
|
-
'accept-
|
|
3546
|
-
'
|
|
3547
|
-
'dnt': '1',
|
|
3548
|
-
'pragma': 'no-cache',
|
|
3549
|
-
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
|
3550
|
-
'sec-ch-ua-mobile': '?0',
|
|
3551
|
-
'sec-ch-ua-platform': '"macOS"',
|
|
3552
|
-
'sec-fetch-dest': 'document',
|
|
3553
|
-
'sec-fetch-mode': 'navigate',
|
|
3554
|
-
'sec-fetch-site': 'none',
|
|
3555
|
-
'sec-fetch-user': '?1',
|
|
3556
|
-
'sec-gpc': '1',
|
|
3557
|
-
'upgrade-insecure-requests': '1',
|
|
3563
|
+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
3564
|
+
'accept-encoding': 'gzip, deflate, br, zstd',
|
|
3565
|
+
'referer': 'https://www.fubo.tv',
|
|
3558
3566
|
'user-agent': USER_AGENT
|
|
3559
3567
|
},
|
|
3560
|
-
json: true,
|
|
3561
3568
|
gzip: true
|
|
3562
3569
|
}
|
|
3563
3570
|
var response = await this.httpGet(reqObj, false)
|
|
3564
3571
|
if ( response ) {
|
|
3565
|
-
|
|
3572
|
+
// disabled because it's big
|
|
3573
|
+
//this.debuglog(response)
|
|
3566
3574
|
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
}
|
|
3579
|
-
break
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3575
|
+
let nextdatastring = response.match(/<script id=\"__NEXT_DATA__\" type=\"application\/json\">(.*?)<\/script>/)
|
|
3576
|
+
let nextdata = JSON.parse(nextdatastring[1])
|
|
3577
|
+
let initialState = JSON.parse(nextdata.props.pageProps.initialState.replace(/\\"/g, '"'))
|
|
3578
|
+
|
|
3579
|
+
initialState.channel.channelPrograms.live.data.forEach((program) => {
|
|
3580
|
+
program.airings.forEach((airing) => {
|
|
3581
|
+
let est_date = new Date(airing.accessRightsV2.live.startTime).toLocaleString("en-US", {timeZone: 'America/New_York'})
|
|
3582
|
+
let date_array = est_date.split(',')[0].split('/')
|
|
3583
|
+
let this_datestring = date_array[2] + '-' + date_array[0].padStart(2, '0') + '-' + date_array[1].padStart(2, '0')
|
|
3584
|
+
if ( !this.cache.bigInningSchedule[this_datestring] || !this.cache.bigInningSchedule[this_datestring].length ) {
|
|
3585
|
+
this.cache.bigInningSchedule[this_datestring] = []
|
|
3582
3586
|
}
|
|
3583
|
-
|
|
3584
|
-
|
|
3587
|
+
this.cache.bigInningSchedule[this_datestring].push({
|
|
3588
|
+
start: airing.accessRightsV2.live.startTime,
|
|
3589
|
+
end: airing.accessRightsV2.live.endTime
|
|
3590
|
+
})
|
|
3591
|
+
});
|
|
3592
|
+
});
|
|
3585
3593
|
this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
|
|
3586
3594
|
|
|
3587
3595
|
// Default cache period is 1 day from now
|
|
@@ -3608,35 +3616,6 @@ class sessionClass {
|
|
|
3608
3616
|
}
|
|
3609
3617
|
}
|
|
3610
3618
|
|
|
3611
|
-
// Generate generic Big Inning schedule for specified date
|
|
3612
|
-
// times in UTC (and DST) according to https://www.mlb.com/live-stream-games/help-center/subscription-access-big-inning
|
|
3613
|
-
async generateBigInningSchedule(dateString) {
|
|
3614
|
-
try {
|
|
3615
|
-
this.debuglog('generateBigInningSchedule')
|
|
3616
|
-
|
|
3617
|
-
let utc_start_string = '01:00'
|
|
3618
|
-
let utc_end_string = '03:30'
|
|
3619
|
-
let add_date = 1
|
|
3620
|
-
// Different Sunday schedule
|
|
3621
|
-
let weekday_index = new Date(dateString + ' 00:00:00').getDay()
|
|
3622
|
-
if ( weekday_index == 0 ) {
|
|
3623
|
-
utc_start_string = '19:00'
|
|
3624
|
-
utc_end_string = '21:30'
|
|
3625
|
-
add_date = 0
|
|
3626
|
-
}
|
|
3627
|
-
let d = new Date(dateString + 'T' + utc_start_string + ':00.000+00:00')
|
|
3628
|
-
d.setDate(d.getDate()+add_date)
|
|
3629
|
-
let start = d
|
|
3630
|
-
d = new Date(dateString + 'T' + utc_end_string + ':00.000+00:00')
|
|
3631
|
-
d.setDate(d.getDate()+add_date)
|
|
3632
|
-
let end = d
|
|
3633
|
-
|
|
3634
|
-
return {start: start, end: end}
|
|
3635
|
-
} catch(e) {
|
|
3636
|
-
this.log('generateBigInningSchedule error : ' + e.message)
|
|
3637
|
-
}
|
|
3638
|
-
}
|
|
3639
|
-
|
|
3640
3619
|
// Get event data
|
|
3641
3620
|
async getEventData(url) {
|
|
3642
3621
|
try {
|
|
@@ -4349,6 +4328,7 @@ class sessionClass {
|
|
|
4349
4328
|
if ( cache_data ) {
|
|
4350
4329
|
if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
|
|
4351
4330
|
let team_data = this.temp_cache.gamechanger[id].streamFinderData.team_data
|
|
4331
|
+
let games_CLI = this.temp_cache.gamechanger[id].streamFinderData.games_CLI
|
|
4352
4332
|
|
|
4353
4333
|
var games = []
|
|
4354
4334
|
|
|
@@ -5615,6 +5595,56 @@ class sessionClass {
|
|
|
5615
5595
|
this.log('getComskipMarkers error : ' + e.message)
|
|
5616
5596
|
}
|
|
5617
5597
|
}
|
|
5598
|
+
|
|
5599
|
+
// generates AFFILIATE_TEAM_IDS, should be done each season
|
|
5600
|
+
async getAffiliates() {
|
|
5601
|
+
try {
|
|
5602
|
+
this.debuglog('getAffiliates')
|
|
5603
|
+
|
|
5604
|
+
let affiliates_data = {}
|
|
5605
|
+
let reqObj = {
|
|
5606
|
+
url: 'https://statsapi.mlb.com/api/v1/teams?sportIds=1,11,12,13,14&activeStatus=true&season=2026',
|
|
5607
|
+
headers: {
|
|
5608
|
+
'User-agent': USER_AGENT,
|
|
5609
|
+
'Origin': 'https://www.mlb.com',
|
|
5610
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
5611
|
+
'Content-type': 'application/json'
|
|
5612
|
+
},
|
|
5613
|
+
gzip: true
|
|
5614
|
+
}
|
|
5615
|
+
var response = await this.httpGet(reqObj, false)
|
|
5616
|
+
if ( response && this.isValidJson(response) ) {
|
|
5617
|
+
//this.debuglog(response)
|
|
5618
|
+
let teams_data = JSON.parse(response)
|
|
5619
|
+
|
|
5620
|
+
let parent_orgs = {}
|
|
5621
|
+
if ( teams_data && teams_data.teams ) {
|
|
5622
|
+
for (var i=0; i<teams_data.teams.length; i++) {
|
|
5623
|
+
if (teams_data.teams[i].sport.id == 1) {
|
|
5624
|
+
parent_orgs[teams_data.teams[i].id] = teams_data.teams[i].abbreviation
|
|
5625
|
+
affiliates_data[teams_data.teams[i].abbreviation] = []
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
for (var i=0; i<teams_data.teams.length; i++) {
|
|
5629
|
+
if (teams_data.teams[i].sport.id != 1) {
|
|
5630
|
+
teams_data.teams[i].abbreviation
|
|
5631
|
+
affiliates_data[parent_orgs[teams_data.teams[i].parentOrgId]].push(teams_data.teams[i].id)
|
|
5632
|
+
affiliates_data[parent_orgs[teams_data.teams[i].parentOrgId]].sort((a, b) => a - b)
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
for (const [key, value] of Object.entries(affiliates_data)) {
|
|
5636
|
+
affiliates_data[key] = value.join(',')
|
|
5637
|
+
}
|
|
5638
|
+
|
|
5639
|
+
console.log(JSON.stringify(this.sortObj(affiliates_data)))
|
|
5640
|
+
}
|
|
5641
|
+
} else {
|
|
5642
|
+
this.log('error : invalid json from url ' + reqObj.url)
|
|
5643
|
+
}
|
|
5644
|
+
} catch(e) {
|
|
5645
|
+
this.log('getAffiliates error : ' + e.message)
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5618
5648
|
}
|
|
5619
5649
|
|
|
5620
5650
|
module.exports = sessionClass
|