mlbserver 2026.3.27-2 → 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/Dockerfile +5 -40
- package/index.js +190 -40
- package/package.json +1 -2
- package/session.js +74 -159
package/Dockerfile
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
FROM node:18-alpine AS build
|
|
1
|
+
FROM node:16-alpine
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
ENV PUPPETEER_SKIP_DOWNLOAD=true
|
|
6
|
-
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
|
7
|
-
|
|
8
|
-
# Install system-level dependencies for Chromium on Alpine
|
|
9
|
-
RUN apk update && apk add --no-cache \
|
|
10
|
-
tzdata \
|
|
11
|
-
udev \
|
|
12
|
-
ttf-freefont \
|
|
13
|
-
chromium \
|
|
14
|
-
nss \
|
|
15
|
-
freetype \
|
|
16
|
-
harfbuzz \
|
|
17
|
-
ca-certificates
|
|
3
|
+
RUN apk update && apk add tzdata
|
|
18
4
|
|
|
19
5
|
# Create app directory
|
|
20
6
|
WORKDIR /mlbserver
|
|
21
7
|
|
|
8
|
+
# Add data directory
|
|
9
|
+
VOLUME /mlbserver/data_directory
|
|
10
|
+
|
|
22
11
|
# Install app dependencies
|
|
23
12
|
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
|
24
13
|
# where available (npm@5+)
|
|
@@ -31,29 +20,5 @@ RUN npm install
|
|
|
31
20
|
# Bundle app source
|
|
32
21
|
COPY . .
|
|
33
22
|
|
|
34
|
-
# --- Runtime Stage ---
|
|
35
|
-
FROM node:20-alpine AS runtime
|
|
36
|
-
|
|
37
|
-
# Install only the necessary runtime dependencies again
|
|
38
|
-
RUN apk add --no-cache \
|
|
39
|
-
tzdata \
|
|
40
|
-
udev \
|
|
41
|
-
ttf-freefont \
|
|
42
|
-
chromium \
|
|
43
|
-
nss \
|
|
44
|
-
freetype \
|
|
45
|
-
harfbuzz \
|
|
46
|
-
ca-certificates
|
|
47
|
-
|
|
48
|
-
# Set the executable path for Puppeteer
|
|
49
|
-
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
|
50
|
-
|
|
51
|
-
WORKDIR /mlbserver
|
|
52
|
-
# Copy built application from the build stage
|
|
53
|
-
COPY --from=build /mlbserver .
|
|
54
|
-
|
|
55
|
-
# Add data directory
|
|
56
|
-
VOLUME /mlbserver/data_directory
|
|
57
|
-
|
|
58
23
|
EXPOSE 9999 10000
|
|
59
24
|
CMD [ "node", "index.js", "--env", "--port", "9999", "--multiview_port", "10000", "--data_directory", "/mlbserver/data_directory" ]
|
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)
|
|
@@ -1542,8 +1543,7 @@ app.get('/', async function(req, res) {
|
|
|
1542
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}'
|
|
1543
1544
|
|
|
1544
1545
|
// Highlights CSS
|
|
1545
|
-
|
|
1546
|
-
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;}'
|
|
1547
1547
|
|
|
1548
1548
|
// Tooltip CSS
|
|
1549
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;}'
|
|
@@ -1829,37 +1829,38 @@ app.get('/', async function(req, res) {
|
|
|
1829
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') ) {
|
|
1830
1830
|
// Scraped Big Inning schedule
|
|
1831
1831
|
big_inning = await session.getBigInningSchedule(gameDate)
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
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"
|
|
1854
1862
|
}
|
|
1855
|
-
querystring += content_protect_b
|
|
1856
|
-
multiviewquerystring += content_protect_b
|
|
1857
|
-
body += '<a href="' + thislink + querystring + '">Big Inning</a>'
|
|
1858
|
-
body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
|
|
1859
|
-
} else {
|
|
1860
|
-
body += 'Big Inning'
|
|
1861
1863
|
}
|
|
1862
|
-
body += '</td></tr>' + "\n"
|
|
1863
1864
|
}
|
|
1864
1865
|
|
|
1865
1866
|
// Game Changer and Stream Finder
|
|
@@ -3320,11 +3321,13 @@ function start_multiview_stream(streams, sync, dvr, faster, reencode, park_audio
|
|
|
3320
3321
|
//let audio_input = audio_present[i] + ':a:m:language:en?'
|
|
3321
3322
|
let audio_input = audio_present[i] + ':a:'
|
|
3322
3323
|
let video_url = streams[audio_present[i]]
|
|
3323
|
-
|
|
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=') ) {
|
|
3324
3327
|
audio_input += '0'
|
|
3325
|
-
} else {
|
|
3328
|
+
/*} else {
|
|
3326
3329
|
audio_input += '1'
|
|
3327
|
-
}
|
|
3330
|
+
}*/
|
|
3328
3331
|
let filter = ''
|
|
3329
3332
|
// Optionally apply sync adjustments
|
|
3330
3333
|
if ( sync[audio_present[i]] ) {
|
|
@@ -3531,11 +3534,13 @@ app.get('/download.ts', async function(req, res) {
|
|
|
3531
3534
|
if ( ! (await protect(req, res)) ) return
|
|
3532
3535
|
|
|
3533
3536
|
try {
|
|
3537
|
+
let ffmpeg_timeout = 432000
|
|
3534
3538
|
// we'll know it's an actual download request if it include a filename parameter
|
|
3535
3539
|
if ( req.query.filename ) {
|
|
3536
3540
|
session.requestlog('download.ts', req)
|
|
3537
3541
|
} else {
|
|
3538
3542
|
session.debuglog('force alternate audio', req)
|
|
3543
|
+
ffmpeg_timeout = 20
|
|
3539
3544
|
}
|
|
3540
3545
|
|
|
3541
3546
|
let server = 'http://127.0.0.1:' + session.data.port + http_root
|
|
@@ -3561,7 +3566,7 @@ app.get('/download.ts', async function(req, res) {
|
|
|
3561
3566
|
}
|
|
3562
3567
|
}
|
|
3563
3568
|
|
|
3564
|
-
ffmpeg_command = ffmpeg({ timeout:
|
|
3569
|
+
ffmpeg_command = ffmpeg({ timeout: ffmpeg_timeout })
|
|
3565
3570
|
|
|
3566
3571
|
// Set input stream and minimize ffmpeg startup latency
|
|
3567
3572
|
ffmpeg_command.input(video_url)
|
|
@@ -3619,15 +3624,21 @@ app.get('/download.ts', async function(req, res) {
|
|
|
3619
3624
|
ffmpeg_command.addOutputOption('-f', 'mpegts')
|
|
3620
3625
|
.output(res)
|
|
3621
3626
|
.on('start', function(commandLine) {
|
|
3622
|
-
session.debuglog('download.ts command started')
|
|
3627
|
+
session.debuglog('download.ts command started for ' + video_url)
|
|
3623
3628
|
if ( argv.debug || argv.ffmpeg_logging ) {
|
|
3624
|
-
session.
|
|
3629
|
+
session.log('download.ts command: ' + commandLine)
|
|
3625
3630
|
}
|
|
3626
3631
|
})
|
|
3627
3632
|
.on('error', function(err, stdout, stderr) {
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
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)
|
|
3631
3642
|
})
|
|
3632
3643
|
.on('end', function() {
|
|
3633
3644
|
session.debuglog('download.ts command ended')
|
|
@@ -3795,3 +3806,142 @@ app.get('/comskip.txt', async function(req, res) {
|
|
|
3795
3806
|
res.end('comskip.txt request error, check log')
|
|
3796
3807
|
}
|
|
3797
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mlbserver",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.4.1-2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"http": "0.0.1-security",
|
|
14
14
|
"http-attach": "^1.0.0",
|
|
15
15
|
"minimist": "^1.2.8",
|
|
16
|
-
"puppeteer": "^24.40.0",
|
|
17
16
|
"readline-sync": "^1.4.10",
|
|
18
17
|
"request": "^2.88.2",
|
|
19
18
|
"request-promise": "^4.2.6",
|
package/session.js
CHANGED
|
@@ -8,7 +8,6 @@ const path = require('path')
|
|
|
8
8
|
const readlineSync = require('readline-sync')
|
|
9
9
|
const FileCookieStore = require('tough-cookie-filestore')
|
|
10
10
|
const parseString = require('xml2js').parseString
|
|
11
|
-
const puppeteer = require('puppeteer')
|
|
12
11
|
|
|
13
12
|
const MULTIVIEW_DIRECTORY_NAME = 'multiview'
|
|
14
13
|
|
|
@@ -865,8 +864,6 @@ class sessionClass {
|
|
|
865
864
|
constructor(argv = {}) {
|
|
866
865
|
this.debug = argv.debug
|
|
867
866
|
|
|
868
|
-
this.executablePath = argv.PUPPETEER_EXECUTABLE_PATH
|
|
869
|
-
|
|
870
867
|
let dirname = __dirname
|
|
871
868
|
if ( argv.data_directory ) {
|
|
872
869
|
dirname = argv.data_directory
|
|
@@ -2922,25 +2919,28 @@ class sessionClass {
|
|
|
2922
2919
|
let gameDate = cache_data.dates[i].date
|
|
2923
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] ) {
|
|
2924
2921
|
this.debuglog('getTVData Big Inning active for date ' + cache_data.dates[i].date)
|
|
2925
|
-
// Scraped Big Inning schedule
|
|
2926
|
-
let start = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].start))
|
|
2927
|
-
let stop = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].end))
|
|
2928
|
-
|
|
2929
|
-
// Big Inning calendar ICS
|
|
2930
|
-
let prefix = 'Watch'
|
|
2931
|
-
let location = server + '/embed.html?event=biginning&mediaType=Video&resolution=' + resolution
|
|
2932
|
-
if ( this.protection.content_protect ) location += '&content_protect=' + this.protection.content_protect
|
|
2933
|
-
calendar += await this.generate_ics_event(prefix, new Date(this.cache.bigInningSchedule[gameDate].start), new Date(this.cache.bigInningSchedule[gameDate].end), title, description, location)
|
|
2934
2922
|
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
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
|
+
}
|
|
2941
2940
|
|
|
2942
|
-
|
|
2943
|
-
|
|
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
|
+
}
|
|
2944
2944
|
}
|
|
2945
2945
|
this.debuglog('getTVData completed Big Inning for date ' + cache_data.dates[i].date)
|
|
2946
2946
|
}
|
|
@@ -3542,122 +3542,66 @@ class sessionClass {
|
|
|
3542
3542
|
// temporarily disable Big Inning schedule checking until a new source URL is available
|
|
3543
3543
|
/*this.cache.bigInningSchedule = {}
|
|
3544
3544
|
return*/
|
|
3545
|
-
|
|
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
|
+
|
|
3546
3557
|
let currentDate = new Date()
|
|
3547
3558
|
if ( !this.cache || !this.cache.bigInningScheduleCacheExpiry || (currentDate > new Date(this.cache.bigInningScheduleCacheExpiry)) ) {
|
|
3548
3559
|
if ( !this.cache.bigInningSchedule ) this.cache.bigInningSchedule = {}
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
'
|
|
3555
|
-
'
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
],
|
|
3559
|
-
})
|
|
3560
|
-
const page = await browser.newPage()
|
|
3561
|
-
await page.setUserAgent(USER_AGENT)
|
|
3562
|
-
await page.goto('https://support.mlb.com/s/article/What-Is-MLB-Big-Inning?language=en_US', { waitUntil: 'networkidle0' })
|
|
3563
|
-
const response = await page.content()
|
|
3564
|
-
await browser.close()
|
|
3565
|
-
|
|
3566
|
-
// break HTML into array based on table rows
|
|
3567
|
-
var rows = response.split('<tr ')
|
|
3568
|
-
// start iterating at 2 (after header row)
|
|
3569
|
-
for (var i=2; i<rows.length; i++) {
|
|
3570
|
-
// split HTML row into array with columns
|
|
3571
|
-
let cols = rows[i].split('<td ')
|
|
3572
|
-
|
|
3573
|
-
// define some variables that persist for each row
|
|
3574
|
-
let parts
|
|
3575
|
-
let year
|
|
3576
|
-
let month
|
|
3577
|
-
let day
|
|
3578
|
-
let this_datestring
|
|
3579
|
-
let add_date = 0
|
|
3580
|
-
let d
|
|
3581
|
-
|
|
3582
|
-
// start iterating at 2 (after DOW column)
|
|
3583
|
-
for (var j=2; j<cols.length; j++) {
|
|
3584
|
-
// split on brackets to get column text at resulting array index 0
|
|
3585
|
-
let col = cols[j].split('>')[1].split('<')
|
|
3586
|
-
switch(j){
|
|
3587
|
-
// first column is date
|
|
3588
|
-
case 2:
|
|
3589
|
-
// split date into array
|
|
3590
|
-
// old date format (January 1, 1970) (disabled)
|
|
3591
|
-
/*parts = col[0].split(' ')
|
|
3592
|
-
year = parts[2]
|
|
3593
|
-
// get month index, zero-based
|
|
3594
|
-
month = new Date(Date.parse(parts[0] +" 1, 2021")).getMonth()
|
|
3595
|
-
day = parts[1].substring(0,parts[1].length-3)*/
|
|
3596
|
-
// new date format (01/01/70)
|
|
3597
|
-
parts = col[0].split('/')
|
|
3598
|
-
year = parts[2]
|
|
3599
|
-
if ( year.length == 2 ) {
|
|
3600
|
-
year = '20' + parts[2]
|
|
3601
|
-
}
|
|
3602
|
-
// get month index, zero-based
|
|
3603
|
-
month = parseInt(parts[0]) - 1
|
|
3604
|
-
day = parts[1]
|
|
3605
|
-
this_datestring = new Date(year, month, day).toISOString().substring(0,10)
|
|
3606
|
-
this.cache.bigInningSchedule[this_datestring] = {}
|
|
3607
|
-
// increment month index (not zero-based)
|
|
3608
|
-
month += 1
|
|
3609
|
-
break
|
|
3610
|
-
// remaining columns are times
|
|
3611
|
-
default:
|
|
3612
|
-
let hour
|
|
3613
|
-
let minute = '00'
|
|
3614
|
-
let ampm
|
|
3615
|
-
// if time has colon, split into array on that to get hour and minute parts
|
|
3616
|
-
if ( col[0].indexOf(':') > 0 ) {
|
|
3617
|
-
parts = col[0].split(':')
|
|
3618
|
-
hour = parseInt(parts[0])
|
|
3619
|
-
minute = parts[1].substring(0,2)
|
|
3620
|
-
} else {
|
|
3621
|
-
hour = parseInt(col[0].substring(0,col[0].length-2))
|
|
3622
|
-
}
|
|
3623
|
-
ampm = col[0].substring(col[0].length-2,col[0].length)
|
|
3624
|
-
// convert hour to 24-hour format
|
|
3625
|
-
if ( (ampm == 'PM') || ((hour == 12) && (ampm == 'AM')) ) {
|
|
3626
|
-
hour += 12
|
|
3627
|
-
}
|
|
3628
|
-
// these times are EDT so add 4 for UTC
|
|
3629
|
-
hour += 4
|
|
3630
|
-
// if hour is beyond 23, note we will have to add 1 day
|
|
3631
|
-
if ( hour > 23 ) {
|
|
3632
|
-
add_date = 1
|
|
3633
|
-
hour -= 24
|
|
3634
|
-
}
|
|
3635
|
-
|
|
3636
|
-
d = new Date(this_datestring + 'T' + hour.toString().padStart(2, '0') + ':' + minute.toString().padStart(2, '0') + ':00.000+00:00')
|
|
3637
|
-
d.setDate(d.getDate()+add_date)
|
|
3638
|
-
switch(j){
|
|
3639
|
-
// 3rd column is start time
|
|
3640
|
-
case 3:
|
|
3641
|
-
this.cache.bigInningSchedule[this_datestring].start = d
|
|
3642
|
-
break
|
|
3643
|
-
// 3rd column is end time
|
|
3644
|
-
case 4:
|
|
3645
|
-
this.cache.bigInningSchedule[this_datestring].end = d
|
|
3646
|
-
break
|
|
3647
|
-
}
|
|
3648
|
-
break
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3560
|
+
let reqObj = {
|
|
3561
|
+
url: 'https://www.fubo.tv/welcome/channel/mlb-big-inning',
|
|
3562
|
+
headers: {
|
|
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',
|
|
3566
|
+
'user-agent': USER_AGENT
|
|
3567
|
+
},
|
|
3568
|
+
gzip: true
|
|
3651
3569
|
}
|
|
3652
|
-
this.
|
|
3570
|
+
var response = await this.httpGet(reqObj, false)
|
|
3571
|
+
if ( response ) {
|
|
3572
|
+
// disabled because it's big
|
|
3573
|
+
//this.debuglog(response)
|
|
3574
|
+
|
|
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] = []
|
|
3586
|
+
}
|
|
3587
|
+
this.cache.bigInningSchedule[this_datestring].push({
|
|
3588
|
+
start: airing.accessRightsV2.live.startTime,
|
|
3589
|
+
end: airing.accessRightsV2.live.endTime
|
|
3590
|
+
})
|
|
3591
|
+
});
|
|
3592
|
+
});
|
|
3593
|
+
this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
|
|
3653
3594
|
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3595
|
+
// Default cache period is 1 day from now
|
|
3596
|
+
let oneDayFromNow = new Date()
|
|
3597
|
+
oneDayFromNow.setDate(oneDayFromNow.getDate()+1)
|
|
3598
|
+
let cacheExpiry = oneDayFromNow
|
|
3599
|
+
this.cache.bigInningScheduleCacheExpiry = cacheExpiry
|
|
3659
3600
|
|
|
3660
|
-
|
|
3601
|
+
this.save_cache_data()
|
|
3602
|
+
} else {
|
|
3603
|
+
this.log('error : invalid response from url ' + reqObj.url)
|
|
3604
|
+
}
|
|
3661
3605
|
} else {
|
|
3662
3606
|
this.debuglog('using cached big inning schedule')
|
|
3663
3607
|
}
|
|
@@ -3672,35 +3616,6 @@ class sessionClass {
|
|
|
3672
3616
|
}
|
|
3673
3617
|
}
|
|
3674
3618
|
|
|
3675
|
-
// Generate generic Big Inning schedule for specified date
|
|
3676
|
-
// times in UTC (and DST) according to https://www.mlb.com/live-stream-games/help-center/subscription-access-big-inning
|
|
3677
|
-
async generateBigInningSchedule(dateString) {
|
|
3678
|
-
try {
|
|
3679
|
-
this.debuglog('generateBigInningSchedule')
|
|
3680
|
-
|
|
3681
|
-
let utc_start_string = '01:00'
|
|
3682
|
-
let utc_end_string = '03:30'
|
|
3683
|
-
let add_date = 1
|
|
3684
|
-
// Different Sunday schedule
|
|
3685
|
-
let weekday_index = new Date(dateString + ' 00:00:00').getDay()
|
|
3686
|
-
if ( weekday_index == 0 ) {
|
|
3687
|
-
utc_start_string = '19:00'
|
|
3688
|
-
utc_end_string = '21:30'
|
|
3689
|
-
add_date = 0
|
|
3690
|
-
}
|
|
3691
|
-
let d = new Date(dateString + 'T' + utc_start_string + ':00.000+00:00')
|
|
3692
|
-
d.setDate(d.getDate()+add_date)
|
|
3693
|
-
let start = d
|
|
3694
|
-
d = new Date(dateString + 'T' + utc_end_string + ':00.000+00:00')
|
|
3695
|
-
d.setDate(d.getDate()+add_date)
|
|
3696
|
-
let end = d
|
|
3697
|
-
|
|
3698
|
-
return {start: start, end: end}
|
|
3699
|
-
} catch(e) {
|
|
3700
|
-
this.log('generateBigInningSchedule error : ' + e.message)
|
|
3701
|
-
}
|
|
3702
|
-
}
|
|
3703
|
-
|
|
3704
3619
|
// Get event data
|
|
3705
3620
|
async getEventData(url) {
|
|
3706
3621
|
try {
|