mlbserver 2026.4.1-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/README.md +2 -0
- package/docker-compose.yml +1 -0
- package/index.js +164 -15
- package/package.json +1 -1
- package/session.js +25 -22
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
|
|
|
@@ -1353,31 +1354,92 @@ app.get('/gamechangerplaylist.m3u8', async function(req, res) {
|
|
|
1353
1354
|
})
|
|
1354
1355
|
|
|
1355
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
|
+
|
|
1356
1405
|
async function protect(req, res) {
|
|
1406
|
+
cleanupExpiredSessions();
|
|
1407
|
+
|
|
1357
1408
|
if (argv.page_username && argv.page_password) {
|
|
1358
1409
|
if ( !session.protection.content_protect || !req.query.content_protect || (req.query.content_protect != session.protection.content_protect) ) {
|
|
1359
1410
|
if ( !session.protection.content_protect || !req.query.content_protect || !req.query.content_protect[0] || (req.query.content_protect[0] != session.protection.content_protect) ) {
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
+
}
|
|
1365
1425
|
|
|
1366
|
-
|
|
1426
|
+
const authorization = req.headers.authorization;
|
|
1367
1427
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1428
|
+
if (!authorization) {
|
|
1429
|
+
return reject();
|
|
1430
|
+
}
|
|
1371
1431
|
|
|
1372
|
-
|
|
1432
|
+
const [username, password] = Buffer.from(authorization.replace('Basic ', ''), 'base64').toString().split(':');
|
|
1373
1433
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1434
|
+
if (! (username === argv.page_username && password === argv.page_password)) {
|
|
1435
|
+
return reject();
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1376
1438
|
}
|
|
1377
1439
|
}
|
|
1378
1440
|
}
|
|
1379
1441
|
}
|
|
1380
|
-
return true
|
|
1442
|
+
return true;
|
|
1381
1443
|
}
|
|
1382
1444
|
|
|
1383
1445
|
function getLastName(fullName) {
|
|
@@ -1390,6 +1452,89 @@ function getLastName(fullName) {
|
|
|
1390
1452
|
return fullName.substring(indexOfSpace + 1);
|
|
1391
1453
|
}
|
|
1392
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
|
+
|
|
1393
1538
|
// Server homepage, base URL
|
|
1394
1539
|
app.get('/', async function(req, res) {
|
|
1395
1540
|
try {
|
|
@@ -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
|
package/package.json
CHANGED
package/session.js
CHANGED
|
@@ -3558,38 +3558,41 @@ class sessionClass {
|
|
|
3558
3558
|
if ( !this.cache || !this.cache.bigInningScheduleCacheExpiry || (currentDate > new Date(this.cache.bigInningScheduleCacheExpiry)) ) {
|
|
3559
3559
|
if ( !this.cache.bigInningSchedule ) this.cache.bigInningSchedule = {}
|
|
3560
3560
|
let reqObj = {
|
|
3561
|
-
url: 'https://
|
|
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
3562
|
headers: {
|
|
3563
|
-
'accept': '
|
|
3563
|
+
'accept': '*/*',
|
|
3564
3564
|
'accept-encoding': 'gzip, deflate, br, zstd',
|
|
3565
|
-
'
|
|
3565
|
+
'origin': 'https://www.espn.com',
|
|
3566
|
+
'referer': 'https://www.espn.com/',
|
|
3566
3567
|
'user-agent': USER_AGENT
|
|
3567
3568
|
},
|
|
3569
|
+
json: true,
|
|
3568
3570
|
gzip: true
|
|
3569
3571
|
}
|
|
3570
3572
|
var response = await this.httpGet(reqObj, false)
|
|
3571
3573
|
if ( response ) {
|
|
3572
|
-
|
|
3573
|
-
//this.debuglog(response)
|
|
3574
|
+
this.debuglog(JSON.stringify(response))
|
|
3574
3575
|
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
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] = []
|
|
3587
|
+
}
|
|
3588
|
+
this.cache.bigInningSchedule[big_inning_date].push({
|
|
3589
|
+
start: big_inning_start,
|
|
3590
|
+
end: big_inning_end
|
|
3591
|
+
})
|
|
3592
|
+
break
|
|
3586
3593
|
}
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
end: airing.accessRightsV2.live.endTime
|
|
3590
|
-
})
|
|
3591
|
-
});
|
|
3592
|
-
});
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3593
3596
|
this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
|
|
3594
3597
|
|
|
3595
3598
|
// Default cache period is 1 day from now
|