mlbserver 2026.3.27-2 → 2026.4.16
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/README.md +2 -0
- package/docker-compose.yml +1 -0
- package/index.js +354 -55
- package/package.json +1 -2
- package/session.js +74 -156
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/README.md
CHANGED
|
@@ -50,6 +50,7 @@ Basic command line or Docker environment options:
|
|
|
50
50
|
--session or -s (clears session)
|
|
51
51
|
--cache or -c (clears cache)
|
|
52
52
|
--env or -e (use environment variables instead of command line arguments; automatically applied in the Docker image)
|
|
53
|
+
--login_page or -lp (false if not specified, login is done via a login page and uses cookies)
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
Advanced command line or Docker environment options:
|
|
@@ -68,6 +69,7 @@ Advanced command line or Docker environment options:
|
|
|
68
69
|
--page_username (username to protect pages; default is no protection)
|
|
69
70
|
--page_password (password to protect pages; default is no protection)
|
|
70
71
|
--content_protect (specify the content protection key to include as a URL parameter, if page protection is enabled)
|
|
72
|
+
--login_page (optional, enable login page authentication; default is false, uses browswer popup when unset)
|
|
71
73
|
--gamechanger_delay (specify extra delay for the gamechanger switches in 10 second increments, default is 0)
|
|
72
74
|
--data_directory (defaults to installed application directory; in the Docker image, this defaults to /mlbserver/data_directory for mapping persistent storage)
|
|
73
75
|
```
|
package/docker-compose.yml
CHANGED
package/index.js
CHANGED
|
@@ -129,9 +129,10 @@ var argv = minimist(process.argv, {
|
|
|
129
129
|
s: 'session',
|
|
130
130
|
c: 'cache',
|
|
131
131
|
v: 'version',
|
|
132
|
-
e: 'env'
|
|
132
|
+
e: 'env',
|
|
133
|
+
lp: 'login_page'
|
|
133
134
|
},
|
|
134
|
-
boolean: ['ffmpeg_logging', 'debug', 'logout', 'session', 'cache', 'version', 'free', 'env'],
|
|
135
|
+
boolean: ['ffmpeg_logging', 'debug', 'logout', 'session', 'cache', 'version', 'free', 'env', 'login_page'],
|
|
135
136
|
string: ['account_username', 'account_password', 'fav_teams', 'multiview_path', 'ffmpeg_path', 'ffmpeg_encoder', 'page_username', 'page_password', 'content_protect', 'data_directory', 'http_root']
|
|
136
137
|
})
|
|
137
138
|
|
|
@@ -262,6 +263,7 @@ app.get('/clearcache', async function(req, res) {
|
|
|
262
263
|
session.log('Clearing session...')
|
|
263
264
|
session.clear_session_data()
|
|
264
265
|
session = new sessionClass(argv)
|
|
266
|
+
session.setPorts(port, multiview_port)
|
|
265
267
|
|
|
266
268
|
let server = (req.headers['x-forwarded-proto'] ? req.headers['x-forwarded-proto'] : 'http') + '://' + req.headers.host + http_root
|
|
267
269
|
res.redirect(server)
|
|
@@ -1352,31 +1354,92 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
|
|
|
1352
1354
|
})
|
|
1353
1355
|
|
|
1354
1356
|
// Protect pages by password, or content by content_protect url parameter
|
|
1357
|
+
function isAuthenticated(req) {
|
|
1358
|
+
if (!req.headers.cookie) return false;
|
|
1359
|
+
const cookies = req.headers.cookie.split(';').map(c => c.trim());
|
|
1360
|
+
const authCookie = cookies.find(c => c.startsWith('auth_token='));
|
|
1361
|
+
if (!authCookie) return false;
|
|
1362
|
+
const token = authCookie.split('=')[1];
|
|
1363
|
+
return token === session.protection.content_protect;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const AUTH_TOKEN_TTL = 24 * 60 * 60 * 1000; // 1 day
|
|
1367
|
+
const authenticatedSessions = new Map();
|
|
1368
|
+
|
|
1369
|
+
function isSecure(req) {
|
|
1370
|
+
const forwarded = req.headers['x-forwarded-proto'];
|
|
1371
|
+
if (forwarded) {
|
|
1372
|
+
return forwarded.split(',')[0].trim().toLowerCase() === 'https';
|
|
1373
|
+
}
|
|
1374
|
+
return (req.connection && req.connection.encrypted) || false;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function cleanupExpiredSessions() {
|
|
1378
|
+
const now = Date.now();
|
|
1379
|
+
for (const [token, sessionData] of authenticatedSessions.entries()) {
|
|
1380
|
+
if (sessionData.expiry <= now) {
|
|
1381
|
+
authenticatedSessions.delete(token);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function isAuthenticated(req) {
|
|
1387
|
+
if (!req.headers.cookie) return false;
|
|
1388
|
+
const cookies = req.headers.cookie.split(';').map(c => c.trim());
|
|
1389
|
+
const authCookie = cookies.find(c => c.startsWith('auth_token='));
|
|
1390
|
+
if (!authCookie) return false;
|
|
1391
|
+
|
|
1392
|
+
const token = authCookie.split('=')[1];
|
|
1393
|
+
const sessionData = authenticatedSessions.get(token);
|
|
1394
|
+
if (!sessionData) return false;
|
|
1395
|
+
|
|
1396
|
+
if (sessionData.expiry <= Date.now()) {
|
|
1397
|
+
authenticatedSessions.delete(token);
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
sessionData.expiry = Date.now() + AUTH_TOKEN_TTL;
|
|
1402
|
+
return true;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1355
1405
|
async function protect(req, res) {
|
|
1406
|
+
cleanupExpiredSessions();
|
|
1407
|
+
|
|
1356
1408
|
if (argv.page_username && argv.page_password) {
|
|
1357
1409
|
if ( !session.protection.content_protect || !req.query.content_protect || (req.query.content_protect != session.protection.content_protect) ) {
|
|
1358
1410
|
if ( !session.protection.content_protect || !req.query.content_protect || !req.query.content_protect[0] || (req.query.content_protect[0] != session.protection.content_protect) ) {
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1411
|
+
if (argv.login_page && isAuthenticated(req)) {
|
|
1412
|
+
// Already authenticated via cookie
|
|
1413
|
+
} else {
|
|
1414
|
+
if (argv.login_page) {
|
|
1415
|
+
const redirectUrl = encodeURIComponent(req.url);
|
|
1416
|
+
res.writeHead(302, { 'Location': http_root + '/login?redirect=' + redirectUrl });
|
|
1417
|
+
res.end();
|
|
1418
|
+
return false;
|
|
1419
|
+
} else {
|
|
1420
|
+
const reject = () => {
|
|
1421
|
+
res.setHeader('www-authenticate', 'Basic');
|
|
1422
|
+
res.error(401, ' Not Authorized');
|
|
1423
|
+
return false;
|
|
1424
|
+
}
|
|
1364
1425
|
|
|
1365
|
-
|
|
1426
|
+
const authorization = req.headers.authorization;
|
|
1366
1427
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1428
|
+
if (!authorization) {
|
|
1429
|
+
return reject();
|
|
1430
|
+
}
|
|
1370
1431
|
|
|
1371
|
-
|
|
1432
|
+
const [username, password] = Buffer.from(authorization.replace('Basic ', ''), 'base64').toString().split(':');
|
|
1372
1433
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1434
|
+
if (! (username === argv.page_username && password === argv.page_password)) {
|
|
1435
|
+
return reject();
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1375
1438
|
}
|
|
1376
1439
|
}
|
|
1377
1440
|
}
|
|
1378
1441
|
}
|
|
1379
|
-
return true
|
|
1442
|
+
return true;
|
|
1380
1443
|
}
|
|
1381
1444
|
|
|
1382
1445
|
function getLastName(fullName) {
|
|
@@ -1389,6 +1452,89 @@ function getLastName(fullName) {
|
|
|
1389
1452
|
return fullName.substring(indexOfSpace + 1);
|
|
1390
1453
|
}
|
|
1391
1454
|
|
|
1455
|
+
// Login page
|
|
1456
|
+
app.get('/login', async function(req, res) {
|
|
1457
|
+
try {
|
|
1458
|
+
if (!argv.login_page) {
|
|
1459
|
+
res.error(404, 'Not Found');
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const redirect = req.query.redirect || '/';
|
|
1463
|
+
const error = req.query.error ? '<p style="color:red;">Invalid username or password</p>' : '';
|
|
1464
|
+
res.writeHead(200, {'Content-Type': 'text/html'});
|
|
1465
|
+
res.end(`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Login - ${appname}</title><style>body{color:lightgray;background-color:black;font-family:Arial,Helvetica,sans-serif;}input{background-color:black;color:lightgray;border:1px solid lightgray;}button{background-color:black;color:lightgray;border:1px solid lightgray;}</style></head><body><h1>Login to ${appname}</h1>${error}<form method="POST" action="${http_root}/login"><input type="hidden" name="redirect" value="${redirect}"><p>Username: <input type="text" name="username" required></p><p>Password: <input type="password" name="password" required></p><p><button type="submit">Login</button></p></form></body></html>`);
|
|
1466
|
+
} catch (e) {
|
|
1467
|
+
session.log('login get request error : ' + e.message);
|
|
1468
|
+
res.end('login get request error, check log');
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
function getCookieOptions(req) {
|
|
1473
|
+
const options = ['HttpOnly', 'Path=/', 'SameSite=Strict'];
|
|
1474
|
+
if (isSecure(req)) options.push('Secure');
|
|
1475
|
+
return options.join('; ');
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// Login POST handler
|
|
1479
|
+
app.post('/login', async function(req, res) {
|
|
1480
|
+
try {
|
|
1481
|
+
if (!argv.login_page) {
|
|
1482
|
+
res.error(404, 'Not Found');
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
let body = '';
|
|
1486
|
+
req.on('data', chunk => { body += chunk.toString(); });
|
|
1487
|
+
req.on('end', () => {
|
|
1488
|
+
const params = new URLSearchParams(body);
|
|
1489
|
+
const username = params.get('username');
|
|
1490
|
+
const password = params.get('password');
|
|
1491
|
+
const redirect = params.get('redirect') || '/';
|
|
1492
|
+
if (username === argv.page_username && password === argv.page_password) {
|
|
1493
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
1494
|
+
authenticatedSessions.set(token, { expiry: Date.now() + AUTH_TOKEN_TTL });
|
|
1495
|
+
res.writeHead(302, {
|
|
1496
|
+
'Location': redirect,
|
|
1497
|
+
'Set-Cookie': `auth_token=${token}; ${getCookieOptions(req)}`
|
|
1498
|
+
});
|
|
1499
|
+
res.end();
|
|
1500
|
+
} else {
|
|
1501
|
+
res.writeHead(302, { 'Location': `${http_root}/login?redirect=${encodeURIComponent(redirect)}&error=1` });
|
|
1502
|
+
res.end();
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
} catch (e) {
|
|
1506
|
+
session.log('login post request error : ' + e.message);
|
|
1507
|
+
res.end('login post request error, check log');
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
// Logout
|
|
1512
|
+
app.get('/logout', async function(req, res) {
|
|
1513
|
+
try {
|
|
1514
|
+
if (!argv.login_page) {
|
|
1515
|
+
res.error(404, 'Not Found');
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
let token = '';
|
|
1519
|
+
if (req.headers.cookie) {
|
|
1520
|
+
const cookies = req.headers.cookie.split(';').map(c => c.trim());
|
|
1521
|
+
const authCookie = cookies.find(c => c.startsWith('auth_token='));
|
|
1522
|
+
if (authCookie) token = authCookie.split('=')[1];
|
|
1523
|
+
}
|
|
1524
|
+
if (token && authenticatedSessions.has(token)) {
|
|
1525
|
+
authenticatedSessions.delete(token);
|
|
1526
|
+
}
|
|
1527
|
+
res.writeHead(302, {
|
|
1528
|
+
'Location': '/',
|
|
1529
|
+
'Set-Cookie': `auth_token=; ${getCookieOptions(req)}; Max-Age=0`
|
|
1530
|
+
});
|
|
1531
|
+
res.end();
|
|
1532
|
+
} catch (e) {
|
|
1533
|
+
session.log('logout request error : ' + e.message);
|
|
1534
|
+
res.end('logout request error, check log');
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1392
1538
|
// Server homepage, base URL
|
|
1393
1539
|
app.get('/', async function(req, res) {
|
|
1394
1540
|
try {
|
|
@@ -1542,8 +1688,7 @@ app.get('/', async function(req, res) {
|
|
|
1542
1688
|
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
1689
|
|
|
1544
1690
|
// 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;}'
|
|
1691
|
+
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
1692
|
|
|
1548
1693
|
// Tooltip CSS
|
|
1549
1694
|
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;}'
|
|
@@ -1570,6 +1715,10 @@ app.get('/', async function(req, res) {
|
|
|
1570
1715
|
|
|
1571
1716
|
body += '</script></head><body><h1>' + appname + '</h1>' + "\n"
|
|
1572
1717
|
|
|
1718
|
+
if (argv.login_page) {
|
|
1719
|
+
body += '<p><a href="' + http_root + '/logout">Logout</a></p>' + "\n"
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1573
1722
|
body += '<p><span class="tooltip tinytext">Touch or hover over an option name for more details</span></p>' + "\n"
|
|
1574
1723
|
|
|
1575
1724
|
todayUTCHours -= 4
|
|
@@ -1829,37 +1978,38 @@ app.get('/', async function(req, res) {
|
|
|
1829
1978
|
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
1979
|
// Scraped Big Inning schedule
|
|
1831
1980
|
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
|
-
|
|
1981
|
+
}
|
|
1982
|
+
if ( big_inning ) {
|
|
1983
|
+
for (var i = 0; i < big_inning.length; i++) {
|
|
1984
|
+
if ( big_inning[i].start ) {
|
|
1985
|
+
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>'
|
|
1986
|
+
let compareStart = new Date(big_inning[i].start)
|
|
1987
|
+
compareStart.setMinutes(compareStart.getMinutes()-10)
|
|
1988
|
+
let compareEnd = new Date(big_inning[i].end)
|
|
1989
|
+
compareEnd.setHours(compareEnd.getHours()+1)
|
|
1990
|
+
if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
|
|
1991
|
+
let querystring = '?event=biginning'
|
|
1992
|
+
let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
|
|
1993
|
+
if ( linkType == VALID_LINK_TYPES[0] ) {
|
|
1994
|
+
if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
|
|
1995
|
+
if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
|
|
1996
|
+
}
|
|
1997
|
+
if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
|
|
1998
|
+
if ( linkType == VALID_LINK_TYPES[1] ) {
|
|
1999
|
+
if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
|
|
2000
|
+
} else if ( linkType == VALID_LINK_TYPES[4] ) {
|
|
2001
|
+
querystring += '&filename=' + gameDate + ' Big Inning'
|
|
2002
|
+
}
|
|
2003
|
+
querystring += content_protect_b
|
|
2004
|
+
multiviewquerystring += content_protect_b
|
|
2005
|
+
body += '<a href="' + thislink + querystring + '">Big Inning</a>'
|
|
2006
|
+
body += '<input type="checkbox" value="http://127.0.0.1:' + session.data.port + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
|
|
2007
|
+
} else {
|
|
2008
|
+
body += 'Big Inning'
|
|
2009
|
+
}
|
|
2010
|
+
body += '</td></tr>' + "\n"
|
|
1854
2011
|
}
|
|
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
2012
|
}
|
|
1862
|
-
body += '</td></tr>' + "\n"
|
|
1863
2013
|
}
|
|
1864
2014
|
|
|
1865
2015
|
// Game Changer and Stream Finder
|
|
@@ -3320,11 +3470,13 @@ function start_multiview_stream(streams, sync, dvr, faster, reencode, park_audio
|
|
|
3320
3470
|
//let audio_input = audio_present[i] + ':a:m:language:en?'
|
|
3321
3471
|
let audio_input = audio_present[i] + ':a:'
|
|
3322
3472
|
let video_url = streams[audio_present[i]]
|
|
3323
|
-
|
|
3473
|
+
// disabled code below because we can now assume
|
|
3474
|
+
// the first audio track is the one we want
|
|
3475
|
+
//if ( !video_url || video_url.includes('audio_track=English') || !video_url.includes('audio_track=') ) {
|
|
3324
3476
|
audio_input += '0'
|
|
3325
|
-
} else {
|
|
3477
|
+
/*} else {
|
|
3326
3478
|
audio_input += '1'
|
|
3327
|
-
}
|
|
3479
|
+
}*/
|
|
3328
3480
|
let filter = ''
|
|
3329
3481
|
// Optionally apply sync adjustments
|
|
3330
3482
|
if ( sync[audio_present[i]] ) {
|
|
@@ -3531,11 +3683,13 @@ app.get('/download.ts', async function(req, res) {
|
|
|
3531
3683
|
if ( ! (await protect(req, res)) ) return
|
|
3532
3684
|
|
|
3533
3685
|
try {
|
|
3686
|
+
let ffmpeg_timeout = 432000
|
|
3534
3687
|
// we'll know it's an actual download request if it include a filename parameter
|
|
3535
3688
|
if ( req.query.filename ) {
|
|
3536
3689
|
session.requestlog('download.ts', req)
|
|
3537
3690
|
} else {
|
|
3538
3691
|
session.debuglog('force alternate audio', req)
|
|
3692
|
+
ffmpeg_timeout = 20
|
|
3539
3693
|
}
|
|
3540
3694
|
|
|
3541
3695
|
let server = 'http://127.0.0.1:' + session.data.port + http_root
|
|
@@ -3561,7 +3715,7 @@ app.get('/download.ts', async function(req, res) {
|
|
|
3561
3715
|
}
|
|
3562
3716
|
}
|
|
3563
3717
|
|
|
3564
|
-
ffmpeg_command = ffmpeg({ timeout:
|
|
3718
|
+
ffmpeg_command = ffmpeg({ timeout: ffmpeg_timeout })
|
|
3565
3719
|
|
|
3566
3720
|
// Set input stream and minimize ffmpeg startup latency
|
|
3567
3721
|
ffmpeg_command.input(video_url)
|
|
@@ -3619,15 +3773,21 @@ app.get('/download.ts', async function(req, res) {
|
|
|
3619
3773
|
ffmpeg_command.addOutputOption('-f', 'mpegts')
|
|
3620
3774
|
.output(res)
|
|
3621
3775
|
.on('start', function(commandLine) {
|
|
3622
|
-
session.debuglog('download.ts command started')
|
|
3776
|
+
session.debuglog('download.ts command started for ' + video_url)
|
|
3623
3777
|
if ( argv.debug || argv.ffmpeg_logging ) {
|
|
3624
|
-
session.
|
|
3778
|
+
session.log('download.ts command: ' + commandLine)
|
|
3625
3779
|
}
|
|
3626
3780
|
})
|
|
3627
3781
|
.on('error', function(err, stdout, stderr) {
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3782
|
+
if (err.message.includes('timeout')) {
|
|
3783
|
+
session.debuglog('download.ts command timeout: ' + err.message)
|
|
3784
|
+
} else {
|
|
3785
|
+
session.debuglog('download.ts command error: ' + err.message)
|
|
3786
|
+
}
|
|
3787
|
+
if ( stdout ) session.debuglog(stdout)
|
|
3788
|
+
if ( stderr ) session.debuglog(stderr)
|
|
3789
|
+
ffmpeg_command.kill('SIGKILL')
|
|
3790
|
+
session.debuglog('killed ffmpeg process due to error processing ' + video_url)
|
|
3631
3791
|
})
|
|
3632
3792
|
.on('end', function() {
|
|
3633
3793
|
session.debuglog('download.ts command ended')
|
|
@@ -3795,3 +3955,142 @@ app.get('/comskip.txt', async function(req, res) {
|
|
|
3795
3955
|
res.end('comskip.txt request error, check log')
|
|
3796
3956
|
}
|
|
3797
3957
|
})
|
|
3958
|
+
|
|
3959
|
+
// Listen for embedded MPEGTS stream requests
|
|
3960
|
+
// embedded player not fully tested
|
|
3961
|
+
app.get('/mpegts.html', async function(req, res) {
|
|
3962
|
+
if ( ! (await protect(req, res)) ) return
|
|
3963
|
+
|
|
3964
|
+
try {
|
|
3965
|
+
let server = 'http://127.0.0.1:' + session.data.port + http_root
|
|
3966
|
+
|
|
3967
|
+
let video_url = '/stream.ts'
|
|
3968
|
+
if ( req.query.src ) {
|
|
3969
|
+
video_url = req.query.src
|
|
3970
|
+
} else {
|
|
3971
|
+
let urlArray = req.url.split('?')
|
|
3972
|
+
if ( (urlArray.length == 2) ) {
|
|
3973
|
+
video_url += '?' + urlArray[1]
|
|
3974
|
+
}
|
|
3975
|
+
video_url = server + video_url
|
|
3976
|
+
}
|
|
3977
|
+
session.debuglog('mpegts.html src : ' + video_url)
|
|
3978
|
+
|
|
3979
|
+
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>'
|
|
3980
|
+
|
|
3981
|
+
res.end(body)
|
|
3982
|
+
} catch (e) {
|
|
3983
|
+
session.log('mpegts.html request error : ' + e.message)
|
|
3984
|
+
res.end('')
|
|
3985
|
+
}
|
|
3986
|
+
})
|
|
3987
|
+
|
|
3988
|
+
|
|
3989
|
+
// Listen for MPEGTS stream requests
|
|
3990
|
+
app.get('/stream.ts', async function(req, res) {
|
|
3991
|
+
if ( ! (await protect(req, res)) ) return
|
|
3992
|
+
|
|
3993
|
+
try {
|
|
3994
|
+
let server = 'http://127.0.0.1:' + session.data.port + http_root
|
|
3995
|
+
|
|
3996
|
+
let video_url = '/stream.m3u8'
|
|
3997
|
+
if ( req.query.src ) {
|
|
3998
|
+
video_url = req.query.src
|
|
3999
|
+
} else {
|
|
4000
|
+
let urlArray = req.url.split('?')
|
|
4001
|
+
if ( (urlArray.length == 2) ) {
|
|
4002
|
+
video_url += '?' + urlArray[1]
|
|
4003
|
+
}
|
|
4004
|
+
video_url = server + video_url
|
|
4005
|
+
}
|
|
4006
|
+
session.debuglog('stream.ts src : ' + video_url)
|
|
4007
|
+
|
|
4008
|
+
// force adaptive streams to just use a single video resolution/track
|
|
4009
|
+
if ( !video_url.includes('resolution=') ) {
|
|
4010
|
+
video_url += '&resolution=best'
|
|
4011
|
+
} else if ( video_url.includes('resolution=adaptive') ) {
|
|
4012
|
+
video_url = video_url.replace('resolution=adaptive', 'resolution=best')
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
// force streams to just use a single audio track, if they aren't already
|
|
4016
|
+
if ( !video_url.includes('audio_track=') ) {
|
|
4017
|
+
video_url += '&audio_track=English'
|
|
4018
|
+
} else if ( video_url.includes('audio_track=all') ) {
|
|
4019
|
+
video_url = video_url.replace('audio_track=all', 'audio_track=English')
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
ffmpeg_command = ffmpeg({ timeout: 432000 })
|
|
4023
|
+
|
|
4024
|
+
// Set input live stream and minimize ffmpeg startup latency
|
|
4025
|
+
ffmpeg_command.input(video_url)
|
|
4026
|
+
.addInputOption('-thread_queue_size', '4096')
|
|
4027
|
+
.addInputOption('-fflags', 'nobuffer')
|
|
4028
|
+
.addInputOption('-probesize', '1000000')
|
|
4029
|
+
.addInputOption('-analyzeduration', '0')
|
|
4030
|
+
|
|
4031
|
+
// We'll limit our processing to real-time
|
|
4032
|
+
ffmpeg_command.native()
|
|
4033
|
+
|
|
4034
|
+
let video_input = 0
|
|
4035
|
+
let audio_input = 0
|
|
4036
|
+
|
|
4037
|
+
// Adjust audio sync, if specified
|
|
4038
|
+
if ( req.query.sync ) {
|
|
4039
|
+
if ( req.query.sync > 0 ) {
|
|
4040
|
+
session.log('stream.ts delaying video by ' + req.query.sync + ' seconds')
|
|
4041
|
+
video_input = 1
|
|
4042
|
+
ffmpeg_command.addInputOption('-itsoffset', req.query.sync)
|
|
4043
|
+
} else {
|
|
4044
|
+
session.log('stream.ts delaying audio by ' + (req.query.sync * -1) + ' seconds')
|
|
4045
|
+
audio_input = 1
|
|
4046
|
+
ffmpeg_command.addInputOption('-itsoffset', (req.query.sync * -1))
|
|
4047
|
+
}
|
|
4048
|
+
ffmpeg_command.input(video_url)
|
|
4049
|
+
.addInputOption('-thread_queue_size', '4096')
|
|
4050
|
+
|
|
4051
|
+
// We'll limit our processing to real-time
|
|
4052
|
+
ffmpeg_command.native()
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
// video
|
|
4056
|
+
ffmpeg_command.addOutputOption('-map', video_input + ':v:0')
|
|
4057
|
+
.addOutputOption('-c:v', 'copy')
|
|
4058
|
+
|
|
4059
|
+
// audio
|
|
4060
|
+
ffmpeg_command.addOutputOption('-map', audio_input + ':a')
|
|
4061
|
+
.addOutputOption('-c:a', 'copy')
|
|
4062
|
+
|
|
4063
|
+
// output mpegts to response stream
|
|
4064
|
+
ffmpeg_command.addOutputOption('-f', 'mpegts')
|
|
4065
|
+
.output(res)
|
|
4066
|
+
.on('start', function(commandLine) {
|
|
4067
|
+
session.debuglog('stream.ts command started')
|
|
4068
|
+
if ( argv.debug || argv.ffmpeg_logging ) {
|
|
4069
|
+
session.log('stream.ts command: ' + commandLine)
|
|
4070
|
+
}
|
|
4071
|
+
})
|
|
4072
|
+
.on('error', function(err, stdout, stderr) {
|
|
4073
|
+
session.debuglog('stream.ts command stopped: ' + err.message)
|
|
4074
|
+
if ( stdout ) session.debuglog(stdout)
|
|
4075
|
+
if ( stderr ) session.debuglog(stderr)
|
|
4076
|
+
})
|
|
4077
|
+
.on('end', function() {
|
|
4078
|
+
session.debuglog('stream.ts command ended')
|
|
4079
|
+
})
|
|
4080
|
+
|
|
4081
|
+
if ( argv.ffmpeg_logging ) {
|
|
4082
|
+
session.log('ffmpeg output logging enabled')
|
|
4083
|
+
ffmpeg_command.on('stderr', function(stderrLine) {
|
|
4084
|
+
session.log(stderrLine);
|
|
4085
|
+
})
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
var headers = {'Content-Type': 'video/mp2t',"access-control-allow-origin":"*"}
|
|
4089
|
+
res.writeHead(200, headers)
|
|
4090
|
+
|
|
4091
|
+
ffmpeg_command.run()
|
|
4092
|
+
} catch (e) {
|
|
4093
|
+
session.log('stream.ts request error : ' + e.message)
|
|
4094
|
+
res.end('')
|
|
4095
|
+
}
|
|
4096
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mlbserver",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.4.16",
|
|
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
|
-
channels[channelid].stop = stop
|
|
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))
|
|
2941
2927
|
|
|
2942
|
-
|
|
2943
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
2944
2944
|
}
|
|
2945
2945
|
this.debuglog('getTVData completed Big Inning for date ' + cache_data.dates[i].date)
|
|
2946
2946
|
}
|
|
@@ -3542,122 +3542,69 @@ 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
|
-
|
|
3561
|
-
await
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
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
|
|
3560
|
+
let reqObj = {
|
|
3561
|
+
url: 'https://watch.product.api.espn.com/api/product/v3/watchespn/web/catalog/ae4eb028-0af3-42e7-8965-9304c5817969?lang=en&features=continueWatching%2Csfb-all%2Cpbov7%2Chigh-volume-row%2Csc4u%2Cguide-menu-header%2Ccutl%2Cheader-quickserve%2Cautoplay%2Cwatch-web-redesign%2CimageRatio58x13%2CpromoTiles%2CopenAuthz%2Cvideo-header%2Cexplore-row%2Cbutton-service%2Cinline-header%2Cflagship&deviceBrand=web&streamMenu=true&headerBgImageWidth=1280&countryCode=US&entitlements=no&tz=UTC-0400&userab=espn_watch_for_you_web-392*watch-fy-a-1642',
|
|
3562
|
+
headers: {
|
|
3563
|
+
'accept': '*/*',
|
|
3564
|
+
'accept-encoding': 'gzip, deflate, br, zstd',
|
|
3565
|
+
'origin': 'https://www.espn.com',
|
|
3566
|
+
'referer': 'https://www.espn.com/',
|
|
3567
|
+
'user-agent': USER_AGENT
|
|
3568
|
+
},
|
|
3569
|
+
json: true,
|
|
3570
|
+
gzip: true
|
|
3571
|
+
}
|
|
3572
|
+
var response = await this.httpGet(reqObj, false)
|
|
3573
|
+
if ( response ) {
|
|
3574
|
+
this.debuglog(JSON.stringify(response))
|
|
3575
|
+
|
|
3576
|
+
if ( response.page && response.page.buckets && response.page.buckets[0] && response.page.buckets[0].contents ) {
|
|
3577
|
+
for (var i=0; i < response.page.buckets[0].contents.length; i++) {
|
|
3578
|
+
let content = response.page.buckets[0].contents[i]
|
|
3579
|
+
let big_inning_date = content.utc.substring(0, 10)
|
|
3580
|
+
let big_inning_start = content.utc
|
|
3581
|
+
for (var j=0; j < content.streams.length; j++) {
|
|
3582
|
+
let stream = content.streams[j]
|
|
3583
|
+
let big_inning_end = new Date(content.utc)
|
|
3584
|
+
big_inning_end.setSeconds(stream.durationInSeconds)
|
|
3585
|
+
if ( !this.cache.bigInningSchedule[big_inning_date] || !this.cache.bigInningSchedule[big_inning_date].length ) {
|
|
3586
|
+
this.cache.bigInningSchedule[big_inning_date] = []
|
|
3647
3587
|
}
|
|
3588
|
+
this.cache.bigInningSchedule[big_inning_date].push({
|
|
3589
|
+
start: big_inning_start,
|
|
3590
|
+
end: big_inning_end
|
|
3591
|
+
})
|
|
3648
3592
|
break
|
|
3593
|
+
}
|
|
3649
3594
|
}
|
|
3650
3595
|
}
|
|
3651
|
-
|
|
3652
|
-
this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
|
|
3596
|
+
this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
|
|
3653
3597
|
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3598
|
+
// Default cache period is 1 day from now
|
|
3599
|
+
let oneDayFromNow = new Date()
|
|
3600
|
+
oneDayFromNow.setDate(oneDayFromNow.getDate()+1)
|
|
3601
|
+
let cacheExpiry = oneDayFromNow
|
|
3602
|
+
this.cache.bigInningScheduleCacheExpiry = cacheExpiry
|
|
3659
3603
|
|
|
3660
|
-
|
|
3604
|
+
this.save_cache_data()
|
|
3605
|
+
} else {
|
|
3606
|
+
this.log('error : invalid response from url ' + reqObj.url)
|
|
3607
|
+
}
|
|
3661
3608
|
} else {
|
|
3662
3609
|
this.debuglog('using cached big inning schedule')
|
|
3663
3610
|
}
|
|
@@ -3672,35 +3619,6 @@ class sessionClass {
|
|
|
3672
3619
|
}
|
|
3673
3620
|
}
|
|
3674
3621
|
|
|
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
3622
|
// Get event data
|
|
3705
3623
|
async getEventData(url) {
|
|
3706
3624
|
try {
|