mlbserver 2024.7.25 → 2024.7.27-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.
Files changed (4) hide show
  1. package/README.md +2 -1
  2. package/index.js +56 -5
  3. package/package.json +1 -1
  4. package/session.js +77 -239
package/README.md CHANGED
@@ -1,4 +1,5 @@
1
- [![GitHub release](https://img.shields.io/github/release/tonywagner/mlbserver.svg)](https://github.com/tonywagner/mlbserver/releases)
1
+ [![Docker release](https://img.shields.io/docker/v/tonywagner/mlbserver)](https://hub.docker.com/r/tonywagner/mlbserver)
2
+ [![NPM release](https://img.shields.io/npm/v/mlbserver)](https://www.npmjs.com/package/mlbserver)
2
3
  ![License](https://img.shields.io/badge/license-MIT-blue)
3
4
  [![Contributors](https://img.shields.io/github/contributors/tonywagner/mlbserver.svg)](https://github.com/tonywagner/mlbserver/graphs/contributors)
4
5
 
package/index.js CHANGED
@@ -252,7 +252,7 @@ app.get('/stream.m3u8', async function(req, res) {
252
252
  streamURL = SAMPLE_STREAM_URL
253
253
  options.referer = 'https://hls-js-dev.netlify.app/'
254
254
  } else {
255
- if ( req.query.resolution && (options.resolution == 'best') ) {
255
+ if ( req.query.resolution && (req.query.resolution == 'best') ) {
256
256
  options.resolution = VALID_RESOLUTIONS[1]
257
257
  } else {
258
258
  options.resolution = session.returnValidItem(req.query.resolution, VALID_RESOLUTIONS)
@@ -469,6 +469,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
469
469
 
470
470
  // Some variables for controlling audio/video stream selection, if specified
471
471
  var video_track_matched = false
472
+ var video_track_found = false
472
473
  var audio_track_matched = false
473
474
  var frame_rate = '29.97'
474
475
  if ( (resolution != VALID_RESOLUTIONS[0]) && (resolution != VALID_RESOLUTIONS[VALID_RESOLUTIONS.length-1]) ) {
@@ -588,8 +589,9 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
588
589
  if ( resolution === VALID_RESOLUTIONS[0] ) {
589
590
  return line
590
591
  } else {
591
- if ( line.indexOf(resolution+',FRAME-RATE='+frame_rate) > 0 ) {
592
+ if ( (video_track_found == false) && (line.indexOf(resolution+',FRAME-RATE='+frame_rate) > 0) ) {
592
593
  video_track_matched = true
594
+ video_track_found = true
593
595
  return line
594
596
  } else {
595
597
  return
@@ -1501,6 +1503,32 @@ app.get('/', async function(req, res) {
1501
1503
 
1502
1504
  let currentDate = new Date()
1503
1505
 
1506
+ // MLB Network live stream for paid accounts
1507
+ try {
1508
+ let entitlements = await session.getEntitlements()
1509
+ if ( entitlements.includes('MLBN') || entitlements.includes('MLBALL') || entitlements.includes('MLBTVMLBNADOBEPASS') ) {
1510
+ body += '<tr><td><span class="tooltip">MLB Network<span class="tooltiptext">MLB Network live stream is now included with paid MLBTV subscriptions, or as a paid add-on, in addition to authenticated TV subscribers. <a href="https://www.mlb.com/news/mlb-network-launches-direct-to-consumer-streaming-option">See here for more information</a>.</span></span></td><td>'
1511
+ let querystring = '?event=MLBN'
1512
+ let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1513
+ if ( linkType == VALID_LINK_TYPES[0] ) {
1514
+ if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1515
+ if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1516
+ }
1517
+ if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1518
+ if ( linkType == VALID_LINK_TYPES[1] ) {
1519
+ if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1520
+ }
1521
+ querystring += content_protect_b
1522
+ multiviewquerystring += content_protect_b
1523
+ body += '<a href="' + thislink + querystring + '">MLB Network</a>'
1524
+ body += '<input type="checkbox" value="' + server + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1525
+ body += '</td></tr>' + "\n"
1526
+
1527
+ }
1528
+ } catch (e) {
1529
+ session.debuglog('MLB Network detect error : ' + e.message)
1530
+ }
1531
+
1504
1532
  if ( (mediaType == 'MLBTV') && ((level_ids == levels['MLB']) || level_ids.startsWith(levels['MLB'] + ',')) ) {
1505
1533
  // Recap Rundown beginning in 2023, disabled because it stopped working
1506
1534
  /*if ( (gameDate <= yesterday) && (gameDate >= '2023-03-31') && cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
@@ -2119,6 +2147,8 @@ app.get('/', async function(req, res) {
2119
2147
 
2120
2148
  body += '<p><span class="tooltip">Include by level<span class="tooltiptext">Including a level (AAA, AA, A+ encoded as A%2B, or A, in a comma-separated list if more than 1) will include all of its broadcasts, and exclude all other levels.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeLevels=a%2B,aaa' + content_protect_b + '">m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeLevels=a%2B,aaa' + content_protect_b + '">xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeLevels=a%2B,aaa' + content_protect_b + '">ics</a></p>' + "\n"
2121
2149
 
2150
+ body += '<p><span class="tooltip">Include teams in titles<span class="tooltiptext">An optional parameter added to the URL will include team names in the ICS/XML titles.</span></span>: <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">guide.xml</a> and <a href="/calendar.ics?mediaType=' + mediaType + '&includeTeams=' + include_teams + '&includeTeamsInTitles=true' + content_protect_b + '">calendar.ics</a></p>' + "\n"
2151
+
2122
2152
  body += '</td></tr></table><br/>' + "\n"
2123
2153
 
2124
2154
  body += '<table><tr><td>' + "\n"
@@ -2366,7 +2396,7 @@ app.get('/channels.m3u', async function(req, res) {
2366
2396
  includeOrgs = req.query.includeOrgs.toUpperCase().split(',')
2367
2397
  }
2368
2398
 
2369
- var body = await session.getTVData('channels', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, resolution, pipe, startingChannelNumber)
2399
+ var body = await session.getTVData('channels', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, 'false', resolution, pipe, startingChannelNumber)
2370
2400
 
2371
2401
  res.writeHead(200, {'Content-Type': 'audio/x-mpegurl'})
2372
2402
  res.end(body)
@@ -2408,9 +2438,14 @@ app.get('/calendar.ics', async function(req, res) {
2408
2438
  includeOrgs = req.query.includeOrgs.toUpperCase().split(',')
2409
2439
  }
2410
2440
 
2441
+ let includeTeamsInTitles = 'false'
2442
+ if ( req.query.includeTeamsInTitles ) {
2443
+ includeTeamsInTitles = req.query.includeTeamsInTitles
2444
+ }
2445
+
2411
2446
  let server = 'http://' + req.headers.host
2412
2447
 
2413
- var body = await session.getTVData('calendar', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts)
2448
+ var body = await session.getTVData('calendar', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, includeTeamsInTitles)
2414
2449
 
2415
2450
  res.writeHead(200, {'Content-Type': 'text/calendar'})
2416
2451
  res.end(body)
@@ -2456,9 +2491,25 @@ app.get('/guide.xml', async function(req, res) {
2456
2491
  includeOrgs = req.query.includeOrgs.toUpperCase().split(',')
2457
2492
  }
2458
2493
 
2494
+ let includeTeamsInTitles = 'false'
2495
+ if ( req.query.includeTeamsInTitles ) {
2496
+ includeTeamsInTitles = req.query.includeTeamsInTitles
2497
+ }
2498
+
2459
2499
  let server = 'http://' + req.headers.host
2460
2500
 
2461
- var body = await session.getTVData('guide', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts)
2501
+ var body = await session.getTVData('guide', mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, includeTeamsInTitles)
2502
+
2503
+ res.end(body)
2504
+ })
2505
+
2506
+ // Listen for entitlements requests
2507
+ app.get('/entitlements', async function(req, res) {
2508
+ if ( ! (await protect(req, res)) ) return
2509
+
2510
+ session.requestlog('entitlements', req, true)
2511
+
2512
+ var body = await session.getEntitlements()
2462
2513
 
2463
2514
  res.end(body)
2464
2515
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2024.07.25",
3
+ "version": "2024.07.27-2",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -1472,46 +1472,11 @@ class sessionClass {
1472
1472
  })
1473
1473
  }
1474
1474
 
1475
- // API call
1476
- /*async getApiKeys() {
1477
- this.debuglog('getApiKeys')
1478
- let reqObj = {
1479
- url: 'https://www.mlb.com/tv/g632102/',
1480
- headers: {
1481
- 'User-agent': USER_AGENT,
1482
- 'Origin': 'https://www.mlb.com',
1483
- 'Accept-Encoding': 'gzip, deflate, br'
1484
- },
1485
- gzip: true
1486
- }
1487
- var response = await this.httpGet(reqObj)
1488
- if ( response ) {
1489
- // disabled because it's very big!
1490
- //this.debuglog('getApiKeys response : ' + response)
1491
- var parsed = response.match('"x-api-key","value":"([^"]+)"')
1492
- if ( parsed[1] ) {
1493
- this.data.xApiKey = parsed[1]
1494
- this.save_session_data()
1495
- } else {
1496
- this.log('getApiKeys xApiKey parse failure')
1497
- }
1498
- parsed = response.match('"clientApiKey":"([^"]+)"')
1499
- if ( parsed[1] ) {
1500
- this.data.clientApiKey = parsed[1]
1501
- this.save_session_data()
1502
- } else {
1503
- this.log('getApiKeys clientApiKey parse failure')
1504
- }
1505
- } else {
1506
- this.log('getApiKeys response failure')
1507
- }
1508
- }*/
1509
-
1510
1475
  // new API call
1511
1476
  async getDeviceId() {
1512
1477
  this.debuglog('getDeviceId')
1513
1478
  if ( !this.data.deviceId ) {
1514
- this.getSession()
1479
+ await this.getSession()
1515
1480
  }
1516
1481
  if ( this.data.deviceId ) {
1517
1482
  this.debuglog('using cached deviceId')
@@ -1525,7 +1490,7 @@ class sessionClass {
1525
1490
  async getSessionId() {
1526
1491
  this.debuglog('getSessionId')
1527
1492
  if ( !this.data.sessionId ) {
1528
- this.getSession()
1493
+ await this.getSession()
1529
1494
  }
1530
1495
  if ( this.data.sessionId ) {
1531
1496
  this.debuglog('using cached sessionId')
@@ -1535,6 +1500,20 @@ class sessionClass {
1535
1500
  }
1536
1501
  }
1537
1502
 
1503
+ // new API call
1504
+ async getEntitlements() {
1505
+ this.debuglog('getEntitlements')
1506
+ if ( !this.data.entitlements ) {
1507
+ await this.getSession()
1508
+ }
1509
+ if ( this.data.entitlements ) {
1510
+ this.debuglog('using cached entitlements')
1511
+ return this.data.entitlements
1512
+ } else {
1513
+ this.log('failed to getEntitlements')
1514
+ }
1515
+ }
1516
+
1538
1517
  // new API call
1539
1518
  async getSession() {
1540
1519
  this.debuglog('getSession')
@@ -1578,6 +1557,11 @@ class sessionClass {
1578
1557
  this.debuglog('getSession response : ' + JSON.stringify(response))
1579
1558
  this.data.deviceId = response.data.initSession.deviceId
1580
1559
  this.data.sessionId = response.data.initSession.sessionId
1560
+ var entitlements = []
1561
+ for (var i=0; i<response.data.initSession.entitlements.length; i++) {
1562
+ entitlements.push(response.data.initSession.entitlements[i].code)
1563
+ }
1564
+ this.data.entitlements = entitlements
1581
1565
  this.save_session_data()
1582
1566
  } else {
1583
1567
  this.log('getSession response failure')
@@ -1651,204 +1635,6 @@ class sessionClass {
1651
1635
  }
1652
1636
  }
1653
1637
 
1654
- // API call
1655
- /*async getStreamURL(mediaId) {
1656
- this.debuglog('getStreamURL from ' + mediaId)
1657
- if ( this.cache.media && this.cache.media[mediaId] && this.cache.media[mediaId].streamURL && this.cache.media[mediaId].streamURLExpiry && (Date.parse(this.cache.media[mediaId].streamURLExpiry) > new Date()) ) {
1658
- this.debuglog('using cached streamURL')
1659
- return this.cache.media[mediaId].streamURL
1660
- } else if ( this.cache.media && this.cache.media[mediaId] && this.cache.media[mediaId].blackout && this.cache.media[mediaId].blackoutExpiry && (Date.parse(this.cache.media[mediaId].blackoutExpiry) > new Date()) ) {
1661
- this.log('mediaId recently blacked out, skipping')
1662
- } else {
1663
- let playbackURL = 'https://edge.svcs.mlb.com/media/' + mediaId + '/scenarios/browser~csai'
1664
- let reqObj = {
1665
- url: playbackURL,
1666
- simple: false,
1667
- headers: {
1668
- 'Authorization': await this.getBamAccessToken() || this.halt('missing bamAccessToken'),
1669
- 'User-agent': USER_AGENT,
1670
- 'Accept': 'application/vnd.media-service+json; version=1',
1671
- 'x-bamsdk-version': BAM_SDK_VERSION,
1672
- 'x-bamsdk-platform': PLATFORM,
1673
- 'Origin': 'https://www.mlb.com',
1674
- 'Accept-Encoding': 'gzip, deflate, br',
1675
- 'Content-type': 'application/json'
1676
- },
1677
- gzip: true
1678
- }
1679
- var response = await this.httpGet(reqObj)
1680
- if ( response && this.isValidJson(response) ) {
1681
- this.debuglog('getStreamURL response : ' + response)
1682
- let obj = JSON.parse(response)
1683
- if ( obj.errors ) {
1684
- this.log('getStreamURL error: ' + obj.errors[0])
1685
- if ( obj.errors[0] == 'blackout' ) {
1686
- this.markBlackoutError(mediaId)
1687
- }
1688
- return false
1689
- } else {
1690
- let streamURL = obj.stream.complete
1691
- this.debuglog('getStreamURL : ' + streamURL)
1692
- this.cacheStreamURL(mediaId, streamURL)
1693
- return streamURL
1694
- }
1695
- } else {
1696
- this.log('getStreamURL response failure')
1697
- }
1698
- }
1699
- }
1700
-
1701
- // API call
1702
- async getBamAccessToken() {
1703
- this.debuglog('getBamAccessToken')
1704
- if ( !this.data.bamAccessToken || !this.data.bamAccessTokenExpiry || (Date.parse(this.data.bamAccessTokenExpiry) < new Date()) ) {
1705
- this.debuglog('need to get new bamAccessToken')
1706
- let reqObj = {
1707
- url: BAM_TOKEN_URL,
1708
- headers: {
1709
- 'Authorization': 'Bearer ' + API_KEY,
1710
- 'User-agent': USER_AGENT,
1711
- 'Accept': 'application/vnd.media-service+json; version=1',
1712
- 'x-bamsdk-version': BAM_SDK_VERSION,
1713
- 'x-bamsdk-platform': PLATFORM,
1714
- 'Origin': 'https://www.mlb.com',
1715
- 'Accept-Encoding': 'gzip, deflate, br',
1716
- 'Content-type': 'application/x-www-form-urlencoded'
1717
- },
1718
- form: {
1719
- 'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
1720
- 'platform': 'browser',
1721
- 'subject_token': await this.getEntitlementToken() || this.halt('missing EntitlementToken'),
1722
- 'subject_token_type': 'urn:bamtech:params:oauth:token-type:account'
1723
- },
1724
- gzip: true
1725
- }
1726
- var response = await this.httpPost(reqObj)
1727
- if ( this.isValidJson(response) ) {
1728
- let obj = JSON.parse(response)
1729
- this.debuglog('getBamAccessToken : ' + obj.access_token)
1730
- this.debuglog('getBamAccessToken expires in : ' + obj.expires_in)
1731
- this.data.bamAccessToken = obj.access_token
1732
- this.data.bamAccessTokenExpiry = new Date(new Date().getTime() + obj.expires_in * 1000)
1733
- this.save_session_data()
1734
- return this.data.bamAccessToken
1735
- } else {
1736
- this.log('getBamAccessToken response failure')
1737
- }
1738
- } else {
1739
- return this.data.bamAccessToken
1740
- }
1741
- }
1742
-
1743
- // API call
1744
- async getEntitlementToken() {
1745
- this.debuglog('getEntitlementToken')
1746
- let reqObj = {
1747
- url: 'https://media-entitlement.mlb.com/api/v3/jwt',
1748
- headers: {
1749
- 'Authorization': 'Bearer ' + await this.getLoginToken() || this.halt('missing loginToken'),
1750
- 'Origin': 'https://www.mlb.com'
1751
- },
1752
- qs: {
1753
- 'os': PLATFORM,
1754
- 'did': await this.getDeviceId() || this.halt('missing deviceId'),
1755
- 'appname': 'mlbtv_web'
1756
- }
1757
- }
1758
- var response = await this.httpGet(reqObj)
1759
- if ( response ) {
1760
- this.debuglog('getEntitlementToken response : ' + response)
1761
- this.debuglog('getEntitlementToken : ' + response)
1762
- return response
1763
- } else {
1764
- this.log('getEntitlementToken response failure')
1765
- }
1766
- }
1767
-
1768
- async getDeviceId() {
1769
- this.debuglog('getDeviceId')
1770
- let reqObj = {
1771
- url: 'https://us.edge.bamgrid.com/session',
1772
- headers: {
1773
- 'Authorization': await this.getDeviceAccessToken() || this.halt('missing device_access_token'),
1774
- 'User-agent': USER_AGENT,
1775
- 'Origin': 'https://www.mlb.com',
1776
- 'Accept': 'application/vnd.session-service+json; version=1',
1777
- 'Accept-Encoding': 'gzip, deflate, br',
1778
- 'Accept-Language': 'en-US,en;q=0.5',
1779
- 'x-bamsdk-version': BAM_SDK_VERSION,
1780
- 'x-bamsdk-platform': PLATFORM,
1781
- 'Content-type': 'application/json',
1782
- 'TE': 'Trailers'
1783
- },
1784
- gzip: true
1785
- }
1786
- var response = await this.httpGet(reqObj)
1787
- if ( response && this.isValidJson(response) ) {
1788
- this.debuglog('getDeviceId response : ' + response)
1789
- let obj = JSON.parse(response)
1790
- this.debuglog('getDeviceId : ' + obj.device.id)
1791
- return obj.device.id
1792
- } else {
1793
- this.log('getDeviceId response failure')
1794
- }
1795
- }
1796
-
1797
- // API call
1798
- async getDeviceAccessToken() {
1799
- this.debuglog('getDeviceAccessToken')
1800
- let reqObj = {
1801
- url: BAM_TOKEN_URL,
1802
- headers: {
1803
- 'Authorization': 'Bearer ' + API_KEY,
1804
- 'Origin': 'https://www.mlb.com'
1805
- },
1806
- form: {
1807
- 'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
1808
- 'latitude': '0',
1809
- 'longitude': '0',
1810
- 'platform': 'browser',
1811
- 'subject_token': await this.getDevicesAssertion() || this.halt('missing devicesAssertion'),
1812
- 'subject_token_type': 'urn:bamtech:params:oauth:token-type:device'
1813
- }
1814
- }
1815
- var response = await this.httpPost(reqObj)
1816
- if ( this.isValidJson(response) ) {
1817
- this.debuglog('getDeviceAccessToken response : ' + response)
1818
- let obj = JSON.parse(response)
1819
- this.debuglog('getDeviceAccessToken : ' + obj.access_token)
1820
- return obj.access_token
1821
- } else {
1822
- this.log('getDeviceAccessToken response failure')
1823
- }
1824
- }
1825
-
1826
- // API call
1827
- async getDevicesAssertion() {
1828
- this.debuglog('getDevicesAssertion')
1829
- let reqObj = {
1830
- url: 'https://us.edge.bamgrid.com/devices',
1831
- headers: {
1832
- 'Authorization': 'Bearer ' + API_KEY,
1833
- 'Origin': 'https://www.mlb.com'
1834
- },
1835
- json: {
1836
- 'applicationRuntime': 'firefox',
1837
- 'attributes': {},
1838
- 'deviceFamily': 'browser',
1839
- 'deviceProfile': 'macosx'
1840
- }
1841
- }
1842
- var response = await this.httpPost(reqObj)
1843
- if ( response.assertion ) {
1844
- this.debuglog('getDevicesAssertion response : ' + JSON.stringify(response))
1845
- this.debuglog('getDevicesAssertion : ' + response.assertion)
1846
- return response.assertion
1847
- } else {
1848
- this.log('getDevicesAssertion response failure')
1849
- }
1850
- }*/
1851
-
1852
1638
  // API call
1853
1639
  async getLoginToken() {
1854
1640
  this.debuglog('getLoginToken')
@@ -1870,8 +1656,9 @@ class sessionClass {
1870
1656
  }
1871
1657
  var response = await this.httpPost(reqObj)
1872
1658
  if ( this.isValidJson(response) ) {
1659
+ this.debuglog('getLoginToken : ' + response)
1873
1660
  let obj = JSON.parse(response)
1874
- this.debuglog('getLoginToken : ' + obj.access_token)
1661
+ this.debuglog('getLoginToken token : ' + obj.access_token)
1875
1662
  this.debuglog('getLoginToken expires in : ' + obj.expires_in)
1876
1663
  this.data.loginToken = obj.access_token
1877
1664
  this.data.loginTokenExpiry = new Date(new Date().getTime() + obj.expires_in * 1000)
@@ -2313,7 +2100,7 @@ class sessionClass {
2313
2100
  }
2314
2101
 
2315
2102
  // get TV data (channels or guide)
2316
- async getTVData(dataType, mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, resolution='best', pipe='false', startingChannelNumber=1) {
2103
+ async getTVData(dataType, mediaType, includeTeams, excludeTeams, includeLevels, includeOrgs, server, includeBlackouts, includeTeamsInTitles='false', resolution='best', pipe='false', startingChannelNumber=1) {
2317
2104
  try {
2318
2105
  this.debuglog('getTVData for ' + dataType)
2319
2106
 
@@ -2447,6 +2234,10 @@ class sessionClass {
2447
2234
  let home_team = cache_data.dates[i].games[j].teams['home'].team.name
2448
2235
  let subtitle = away_team + ' at ' + home_team
2449
2236
 
2237
+ if (includeTeamsInTitles == 'true') {
2238
+ title = 'MiLB: ' + subtitle
2239
+ }
2240
+
2450
2241
  let description = cache_data.dates[i].games[j].teams['home'].team.league.name + '. '
2451
2242
  if ( cache_data.dates[i].games[j].seriesDescription != 'Regular Season' ) {
2452
2243
  description += cache_data.dates[i].games[j].seriesDescription + '. '
@@ -2626,6 +2417,17 @@ class sessionClass {
2626
2417
  let home_team = cache_data.dates[i].games[j].teams['home'].team.teamName
2627
2418
  let subtitle = away_team + ' at ' + home_team
2628
2419
 
2420
+ if (includeTeamsInTitles == 'true') {
2421
+ title = 'MLB: ' + subtitle + ' (' + station
2422
+ if ( language == 'es' ) {
2423
+ title += ' Spanish'
2424
+ }
2425
+ if ( mediaType == 'Audio' ) {
2426
+ title += ' Radio'
2427
+ }
2428
+ title += ')'
2429
+ }
2430
+
2629
2431
  let description = station
2630
2432
  if ( mediaType == 'Audio' ) {
2631
2433
  if ( language == 'es' ) {
@@ -2714,6 +2516,40 @@ class sessionClass {
2714
2516
  channels = this.sortObj(channels)
2715
2517
  channels = Object.assign(channels, nationalChannels)
2716
2518
 
2519
+ // MLB Network
2520
+ try {
2521
+ let entitlements = await this.getEntitlements()
2522
+ if ( entitlements.includes('MLBN') || entitlements.includes('MLBALL') || entitlements.includes('MLBTVMLBNADOBEPASS') ) {
2523
+ if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2524
+ if ( (excludeTeams.length > 0) && excludeTeams.includes('MLBN') ) {
2525
+ // do nothing
2526
+ } else if ( (includeTeams.length == 0) || includeTeams.includes('MLBN') ) {
2527
+ this.debuglog('getTVData processing MLB Network')
2528
+ let logo = 'https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcQRgC2JdbtFplKjfhXm5_vzpkUQ3XyDT91SEnHmuB0p5tReQ3Ez'
2529
+ let channelid = mediaType + '.MLBN'
2530
+ if ( this.protection.content_protect ) logo += '&amp;content_protect=' + this.protection.content_protect
2531
+ let stream = server + '/stream.m3u8?event=mlbn&mediaType=Video&resolution=' + resolution
2532
+ if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
2533
+ if ( pipe == 'true' ) stream = await this.convert_stream_to_pipe(stream, channelid)
2534
+ channels[channelid] = await this.create_channel_object(channelid, logo, stream, mediaType)
2535
+
2536
+ let title = 'MLB Network'
2537
+ let description = 'Live stream of MLB Network'
2538
+
2539
+ let start = this.convertDateToXMLTV(new Date(cache_data.dates[0].date + ' 00:00:00'))
2540
+ let stop = this.convertDateToXMLTV(new Date(cache_data.dates[cache_data.dates.length-1].date + ' 00:00:00'))
2541
+
2542
+ // Big Inning guide XML
2543
+ programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertStringToAirDate(cache_data.dates[0].date))
2544
+ this.debuglog('getTVData completed MLB Network')
2545
+ }
2546
+ }
2547
+
2548
+ }
2549
+ } catch (e) {
2550
+ this.debuglog('getTVData MLB Network detect error : ' + e.message)
2551
+ }
2552
+
2717
2553
  // Big Inning
2718
2554
  if ( (mediaType == 'MLBTV') && ((includeLevels.length == 0) || includeLevels.includes('MLB') || includeLevels.includes('ALL')) ) {
2719
2555
  if ( (excludeTeams.length > 0) && excludeTeams.includes('BIGINNING') ) {
@@ -2759,7 +2595,7 @@ class sessionClass {
2759
2595
  calendar += await this.generate_ics_event(prefix, new Date(this.cache.bigInningSchedule[gameDate].start), new Date(this.cache.bigInningSchedule[gameDate].end), title, description, location)
2760
2596
 
2761
2597
  // Big Inning guide XML
2762
- programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertStringToAirDate(this.cache.bigInningSchedule[gameDate].start))
2598
+ programs += await this.generate_xml_program(channelid, start, stop, title, description, logo, this.convertDateToAirDate(new Date(this.cache.bigInningSchedule[gameDate].start)))
2763
2599
  }
2764
2600
  this.debuglog('getTVData completed Big Inning for date ' + cache_data.dates[i].date)
2765
2601
  }
@@ -3535,6 +3371,8 @@ class sessionClass {
3535
3371
  let dateString = eventName.substring(12)
3536
3372
  this.debuglog('getEventStreamURL RecapRundown for ' + dateString)
3537
3373
  playbackURL = await this.getRecapRundownURL(dateString)
3374
+ } else if ( eventName.toUpperCase() == 'MLBN' ) {
3375
+ playbackURL = 'https://falcon.mlbinfra.com/api/v1/linear/mlbn'
3538
3376
  } else {
3539
3377
  playbackURL = await this.getEventURL(eventName)
3540
3378
  }
@@ -4356,7 +4194,7 @@ class sessionClass {
4356
4194
  let xml_output = "\n" + ' <programme channel="' + channelid + '" start="' + start + '" stop="' + stop + '">' + "\n" +
4357
4195
  ' <title lang="en">' + title + '</title>' + "\n"
4358
4196
  if ( subtitle ) {
4359
- xml_output += ' <subtitle lang="en">' + subtitle + '</subtitle>' + "\n"
4197
+ xml_output += ' <sub-title lang="en">' + subtitle + '</sub-title>' + "\n"
4360
4198
  }
4361
4199
  xml_output += ' <desc lang="en">' + description.trim() + '</desc>' + "\n" +
4362
4200
  ' <category lang="en">Sports</category>' + "\n" +