mlbserver 2022.8.8 → 2022.8.23

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 ADDED
@@ -0,0 +1,24 @@
1
+ FROM node:16-alpine
2
+
3
+ RUN apk update && apk add tzdata
4
+
5
+ # Create app directory
6
+ WORKDIR /mlbserver
7
+
8
+ # Add data directory
9
+ VOLUME /mlbserver/data_directory
10
+
11
+ # Install app dependencies
12
+ # A wildcard is used to ensure both package.json AND package-lock.json are copied
13
+ # where available (npm@5+)
14
+ COPY package*.json ./
15
+
16
+ RUN npm install
17
+ # If you are building your code for production
18
+ # RUN npm ci --only=production
19
+
20
+ # Bundle app source
21
+ COPY . .
22
+
23
+ EXPOSE 9999 10000
24
+ CMD [ "node", "index.js", "--env" ]
package/README.md CHANGED
@@ -1,24 +1,94 @@
1
1
  # mlbserver
2
2
 
3
- Current version 2022.08.08
3
+ Current version 2022.08.23
4
4
 
5
5
  Credit to https://github.com/tonycpsu/streamglob and https://github.com/mafintosh/hls-decryptor
6
6
 
7
+ ## Installation
8
+
9
+ ### node-cli
7
10
  ```
8
11
  npm install -g mlbserver
9
12
  ```
10
13
 
11
- ## Usage
14
+ ### docker
15
+ ```
16
+ docker pull tonywagner/mlbserver
17
+ ```
18
+
12
19
 
13
- Launch the server
20
+ ## Launch
14
21
 
22
+ ### node-cli (follow the prompts on first run, or see below for possible command line options)
15
23
  ```
16
24
  mlbserver
17
25
  ```
26
+ or in application directory:
27
+ ```
28
+ node index.js
29
+ ```
30
+
31
+ ### docker-compose
32
+ Update the environment variables below and save it as docker-compose.yml:
33
+ ```
34
+ version: "3"
35
+ services:
36
+ mlbserver:
37
+ image: tonywagner/mlbserver:latest
38
+ container_name: mlbserver
39
+ environment:
40
+ - TZ=America/New York
41
+ - data_directory=/mlbserver/data_directory
42
+ - account_username=your.account.email@example.com
43
+ - account_password=youraccountpassword
44
+ - zip_code=0
45
+ - fav_teams=0
46
+ #- zip_code=00000
47
+ #- fav_teams=ARI,ATL
48
+ #- debug=false
49
+ #- port=9999
50
+ #- multiview_port=10000
51
+ #- multiview_path=
52
+ #- ffmpeg_path=
53
+ #- ffmpeg_encoder=
54
+ #- page_username=
55
+ #- page_password=
56
+ #- content_protect=
57
+ #- gamechanger_delay=0
58
+ ports:
59
+ - 9999:9999
60
+ - 10000:10000
61
+ volumes:
62
+ - /mlbserver/data_directory
63
+ ```
64
+ Then launch it with ```docker-compose up --detach```
65
+
66
+ ### docker-cli
67
+ Update the environment variables in the command below and run it:
68
+ ```
69
+ docker run -d \
70
+ --name mlbserver \
71
+ --env TZ="America/New_York" \
72
+ --env data_directory=/mlbserver/data_directory \
73
+ --env account_username=your.account.email@example.com \
74
+ --env account_password=youraccountpassword \
75
+ --env zip_code=0 \
76
+ --env fav_teams=0 \
77
+ -p 9999:9999 \
78
+ -p 10000:10000 \
79
+ --volume /mlbserver/data_directory \
80
+ tonywagner/mlbserver
81
+ ```
82
+ Subsequent runs can be launched with ```docker start mlbserver```
83
+
84
+ Docker installs may require further configuration to get multiview streaming to work.
85
+
86
+
87
+ ## Usage
18
88
 
19
- and follow the prompts. Load the resulting web URL in a browser to start using the server and to see more documentation.
89
+ After launching the server or Docker container, you can access it at http://localhost:9999 on the same machine, or substitute your computer's IP address for localhost to access it from a different device. Load that address in a web browser to start using the server and to see more documentation.
20
90
 
21
- Basic command line options:
91
+ Basic command line or Docker environment options:
22
92
 
23
93
  ```
24
94
  --port or -p (primary port to run on; defaults to 9999 if not specified)
@@ -27,9 +97,10 @@ Basic command line options:
27
97
  --logout or -l (logs out and clears session)
28
98
  --session or -s (clears session)
29
99
  --cache or -c (clears cache)
100
+ --env or -e (use environment variables instead of command line arguments; necessary for Docker)
30
101
  ```
31
102
 
32
- Advanced command line options:
103
+ Advanced command line or Docker environment options:
33
104
 
34
105
  ```
35
106
  --account_username (email address, default will use stored credentials or prompt user to enter them)
@@ -0,0 +1,29 @@
1
+ version: "3"
2
+ services:
3
+ mlbserver:
4
+ image: tonywagner/mlbserver:latest
5
+ container_name: mlbserver
6
+ environment:
7
+ - TZ=America/New York
8
+ - data_directory=/mlbserver/data_directory
9
+ - account_username=your.account.email@example.com
10
+ - account_password=youraccountpassword
11
+ - zip_code=0
12
+ - fav_teams=0
13
+ #- zip_code=00000
14
+ #- fav_teams=ARI,ATL
15
+ #- debug=false
16
+ #- port=9999
17
+ #- multiview_port=10000
18
+ #- multiview_path=
19
+ #- ffmpeg_path=
20
+ #- ffmpeg_encoder=
21
+ #- page_username=
22
+ #- page_password=
23
+ #- content_protect=
24
+ #- gamechanger_delay=0
25
+ ports:
26
+ - 9999:9999
27
+ - 10000:10000
28
+ volumes:
29
+ - /mlbserver/data_directory
package/index.js CHANGED
@@ -104,9 +104,10 @@ const GAMECHANGER_RESPONSE_HEADERS = {"statusCode":200,"headers":{"content-type"
104
104
  // --account_password (default will use stored credentials or prompt user to enter them)
105
105
  // --zip_code (optional, for USA blackout labels, will prompt if not set or stored)
106
106
  // --fav_teams (optional, comma-separated list of favorite team abbreviations, will prompt if not set or stored)
107
+ // --data_directory (defaults to app directory, must already exist if set to something else; should match storage volume for Docker)
107
108
  // --free (optional, free account, highlights free games)
108
109
  // --multiview_port (port for multiview streaming; defaults to 1 more than primary port, or 10000)
109
- // --multiview_path (where to create the folder for multiview encoded files; defaults to app directory)
110
+ // --multiview_path (where to create the folder for multiview encoded files; defaults to data directory)
110
111
  // --ffmpeg_path (path to ffmpeg binary to use for multiview encoding; default downloads a binary using ffmpeg-static)
111
112
  // --ffmpeg_encoder (ffmpeg video encoder to use for multiview; default is the software encoder libx264)
112
113
  // --ffmpeg_logging (if present, logs all ffmpeg output -- useful for experimenting or troubleshooting)
@@ -125,7 +126,7 @@ var argv = minimist(process.argv, {
125
126
  e: 'env'
126
127
  },
127
128
  boolean: ['ffmpeg_logging', 'debug', 'logout', 'session', 'cache', 'version', 'free', 'env'],
128
- string: ['port', 'account_username', 'account_password', 'multiview_port', 'multiview_path', 'ffmpeg_path', 'ffmpeg_encoder', 'page_username', 'page_password', 'content_protect', 'gamechanger_delay']
129
+ string: ['port', 'account_username', 'account_password', 'multiview_port', 'multiview_path', 'ffmpeg_path', 'ffmpeg_encoder', 'page_username', 'page_password', 'content_protect', 'gamechanger_delay', 'data_directory']
129
130
  })
130
131
 
131
132
  if (argv.env) argv = process.env
@@ -359,20 +360,18 @@ app.get('/stream.m3u8', async function(req, res) {
359
360
  }
360
361
  })
361
362
 
362
- // Store previous keys, for return without decoding
363
- var prevKeys = {}
364
363
  var getKey = function(url, headers, cb) {
365
- if ( (typeof prevKeys[url] !== 'undefined') && (typeof prevKeys[url].key !== 'undefined') ) {
366
- return cb(null, prevKeys[url].key)
364
+ if ( session.temp_cache.prevKeys[url] ) {
365
+ return cb(null, session.temp_cache.prevKeys[url])
367
366
  }
368
367
 
369
- if ( typeof prevKeys[url] === 'undefined' ) prevKeys[url] = {}
370
-
371
368
  session.debuglog('key request : ' + url)
372
369
  requestRetry(url, headers, function(err, response) {
373
370
  if (err) return cb(err)
374
- prevKeys[url].key = response.body
375
- cb(null, response.body)
371
+ let key = response.body
372
+ session.debuglog('key returned ' + key)
373
+ session.temp_cache.prevKeys[url] = key
374
+ cb(null, key)
376
375
  })
377
376
  }
378
377
 
@@ -582,7 +581,13 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
582
581
  // if user specified "none" for video track
583
582
  if ( resolution == VALID_RESOLUTIONS[VALID_RESOLUTIONS.length-1] ) {
584
583
  audio_track_matched = true
585
- audio_output = '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="' + key + '",AUTOSELECT=YES,DEFAULT=YES' + "\n" + '#EXT-X-STREAM-INF:BANDWIDTH=50000,CODECS="mp4a.40.2",AUDIO="aac"' + "\n" + newurl
584
+ audio_output = '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="' + key + '",LANGUAGE="'
585
+ if ( key == ALTERNATE_AUDIO_TRACKS[1] ) {
586
+ audio_output += 'es'
587
+ } else {
588
+ audio_output += 'en'
589
+ }
590
+ audio_output += '",AUTOSELECT=YES,DEFAULT=YES' + "\n" + '#EXT-X-STREAM-INF:BANDWIDTH=50000,CODECS="mp4a.40.2",AUDIO="aac"' + "\n" + newurl
586
591
  } else {
587
592
  if (audio_output != '') audio_output += "\n"
588
593
  audio_output += '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="' + key + '",AUTOSELECT=YES,DEFAULT='
@@ -768,13 +773,16 @@ app.get('/playlist', async function(req, res) {
768
773
  session.debuglog('key line : ' + line)
769
774
  var parsed = line.match(/URI="([^"]+)"(?:,IV=(.+))?$/)
770
775
  if ( parsed ) {
771
- if ( parsed[1].substr(0,4) == 'http' ) key = parsed[1]
772
- else key = url.resolve(u, parsed[1])
773
- session.debuglog('found key : ' + key)
774
- if ( key.startsWith('data:;base64,') ) {
776
+ if ( parsed[1].startsWith('http') ) {
777
+ key = parsed[1]
778
+ session.debuglog('key url : ' + key)
779
+ } else if ( key.startsWith('data:;base64,') ) {
775
780
  let newparsed = key.split(',')
776
781
  key = newparsed[1]
777
- session.debuglog('found new key : ' + key)
782
+ session.debuglog('found key data : ' + key)
783
+ } else {
784
+ key = url.resolve(u, parsed[1])
785
+ session.debuglog('resolved key url : ' + key)
778
786
  }
779
787
  if (parsed[2]) iv = parsed[2].slice(2).toLowerCase()
780
788
  }
@@ -783,8 +791,10 @@ app.get('/playlist', async function(req, res) {
783
791
 
784
792
  if (line[0] === '#') return line
785
793
 
786
- if ( key ) return '/ts?url='+encodeURIComponent(url.resolve(u, line.trim()))+'&key='+encodeURIComponent(key)+'&iv='+encodeURIComponent(iv) + content_protect + referer_parameter
787
- else return '/ts?url='+encodeURIComponent(url.resolve(u, line.trim())) + content_protect + referer_parameter
794
+ let newline = '/ts?url='+encodeURIComponent(url.resolve(u, line.trim())) + content_protect + referer_parameter
795
+ if ( key ) newline += '&key='+encodeURIComponent(key) + '&iv='+encodeURIComponent(iv)
796
+
797
+ return newline
788
798
  })
789
799
  .filter(function(line) {
790
800
  return line
@@ -849,19 +859,8 @@ app.get('/ts', async function(req, res) {
849
859
  if (!req.query.key) return respond(response, res, response.body)
850
860
 
851
861
  try {
852
- //var ku = url.resolve(manifest, req.query.key)
853
862
  var ku = req.query.key
854
- if ( ku.substr(0,4) != 'http' ) {
855
- var iv = Buffer.from(req.query.iv, 'hex')
856
- session.debuglog('iv : 0x'+req.query.iv)
857
-
858
- let key = Buffer.from(ku, "base64")
859
-
860
- var dc = crypto.createDecipheriv('aes-128-cbc', key, iv)
861
- var buffer = Buffer.concat([dc.update(response.body), dc.final()])
862
-
863
- respond(response, res, buffer)
864
- } else {
863
+ if ( ku.startsWith('http') ) {
865
864
  getKey(ku, headers, function(err, key) {
866
865
  if (err) return res.error(err)
867
866
 
@@ -873,6 +872,16 @@ app.get('/ts', async function(req, res) {
873
872
 
874
873
  respond(response, res, buffer)
875
874
  })
875
+ } else {
876
+ var iv = Buffer.from(req.query.iv, 'hex')
877
+ session.debuglog('iv : 0x'+req.query.iv)
878
+
879
+ let key = Buffer.from(ku, "base64")
880
+
881
+ var dc = crypto.createDecipheriv('aes-128-cbc', key, iv)
882
+ var buffer = Buffer.concat([dc.update(response.body), dc.final()])
883
+
884
+ respond(response, res, buffer)
876
885
  }
877
886
  } catch (e) {
878
887
  session.log('key decode error : ' + e.message)
@@ -1267,7 +1276,7 @@ app.get('/', async function(req, res) {
1267
1276
  body += 'function makeGETRequest(url, callback){var request=new XMLHttpRequest();request.onreadystatechange=function(){if (request.readyState==4 && request.status==200){callback(request.responseText)}};request.open("GET", url);request.send();}' + "\n"
1268
1277
 
1269
1278
  // Multiview functions
1270
- body += 'function parsemultiviewresponse(responsetext){if (responsetext == "started"){setTimeout(function(){document.getElementById("startmultiview").innerHTML="Restart";document.getElementById("stopmultiview").innerHTML="Stop"},15000)}else if (responsetext == "stopped"){setTimeout(function(){document.getElementById("stopmultiview").innerHTML="Stopped";document.getElementById("startmultiview").innerHTML="Start"},3000)}else{alert(responsetext)}}function addmultiview(e){for(var i=1;i<=4;i++){var valuefound = false;var oldvalue="";var newvalue=e.value;if(!e.checked){oldvalue=e.value;newvalue=""}if (document.getElementById("multiview" + i).value == oldvalue){document.getElementById("multiview" + i).value=newvalue;valuefound=true;break}}if(e.checked && !valuefound){e.checked=false}}function startmultiview(e){var count=0;var getstr="";for(var i=1;i<=4;i++){if (document.getElementById("multiview"+i).value != ""){count++;getstr+="streams="+encodeURIComponent(document.getElementById("multiview"+i).value)+"&sync="+encodeURIComponent(document.getElementById("sync"+i).value)+"&"}}if((count >= 1) && (count <= 4)){if (document.getElementById("faster").checked){getstr+="faster=true&dvr=true&"}else if (document.getElementById("dvr").checked){getstr+="dvr=true&"}if (document.getElementById("reencode").checked){getstr+="reencode=true&"}if (document.getElementById("audio_url").value != ""){getstr+="audio_url="+encodeURIComponent(document.getElementById("audio_url").value)+"&";if (document.getElementById("audio_url_seek").value != "0"){getstr+="audio_url_seek="+encodeURIComponent(document.getElementById("audio_url_seek").value)}}e.innerHTML="starting...";makeGETRequest("/multiview?"+getstr, parsemultiviewresponse)}else{alert("Multiview requires between 1-4 streams to be selected")}return false}function stopmultiview(e){e.innerHTML="stopping...";makeGETRequest("/multiview", parsemultiviewresponse);return false}' + "\n"
1279
+ body += 'var excludeTeams=[];function parsemultiviewresponse(responsetext){if (responsetext == "started"){setTimeout(function(){document.getElementById("startmultiview").innerHTML="Restart";document.getElementById("stopmultiview").innerHTML="Stop"},15000)}else if (responsetext == "stopped"){setTimeout(function(){document.getElementById("stopmultiview").innerHTML="Stopped";document.getElementById("startmultiview").innerHTML="Start"},3000)}else{alert(responsetext)}}function addmultiview(e, teams=[], excludes=[]){var newvalue=e.value;for(var i=1;i<=4;i++){var valuefound = false;var oldvalue="";if(!e.checked){oldvalue=e.value;newvalue=""}if ((document.getElementById("multiview" + i).value == oldvalue) || ((oldvalue != "") && (document.getElementById("multiview" + i).value.startsWith(oldvalue)))){if ((newvalue != "") && (excludes.length > 0)){newvalue+="&excludeTeams="+excludeTeams.toString()}document.getElementById("multiview" + i).value=newvalue;valuefound=true;break}}if(e.checked && !valuefound){e.checked=false}for(var i=0;i<teams.length;i++){if(e.checked){excludeTeams.push(teams[i])}else{var index=excludeTeams.indexOf(teams[i]);if (index !== -1){excludeTeams.splice(index,1)}}}}function startmultiview(e){var count=0;var getstr="";for(var i=1;i<=4;i++){if (document.getElementById("multiview"+i).value != ""){count++;getstr+="streams="+encodeURIComponent(document.getElementById("multiview"+i).value)+"&sync="+encodeURIComponent(document.getElementById("sync"+i).value)+"&"}}if((count >= 1) && (count <= 4)){if (document.getElementById("faster").checked){getstr+="faster=true&dvr=true&"}else if (document.getElementById("dvr").checked){getstr+="dvr=true&"}if (document.getElementById("reencode").checked){getstr+="reencode=true&"}if (document.getElementById("audio_url").value != ""){getstr+="audio_url="+encodeURIComponent(document.getElementById("audio_url").value)+"&";if (document.getElementById("audio_url_seek").value != "0"){getstr+="audio_url_seek="+encodeURIComponent(document.getElementById("audio_url_seek").value)}}e.innerHTML="starting...";makeGETRequest("/multiview?"+getstr, parsemultiviewresponse)}else{alert("Multiview requires between 1-4 streams to be selected")}return false}function stopmultiview(e){e.innerHTML="stopping...";makeGETRequest("/multiview", parsemultiviewresponse);return false}' + "\n"
1271
1280
 
1272
1281
  // Function to switch URLs to stream URLs, where necessary
1273
1282
  body += 'function stream_substitution(url){return url.replace(/\\/([a-zA-Z]+\.html)/,"/stream.m3u8")}' + "\n"
@@ -1429,15 +1438,14 @@ app.get('/', async function(req, res) {
1429
1438
  body += '<tr><td><span class="tooltip">' + compareStart.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ' - ' + compareEnd.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + '<span class="tooltiptext">The game changer stream will automatically switch between the highest leverage active live non-blackout games, and should be available whenever there are such games available. Does not support adaptive bitrate switching, will default to best resolution if not specified.</span></span></td><td>'
1430
1439
  if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1431
1440
  let streamURL = server + '/gamechanger.m3u8'
1432
- let multiviewquerystring = streamURL + '?resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1433
- multiviewquerystring += content_protect_b
1434
- if ( resolution != VALID_RESOLUTIONS[0] ) streamURL += '?resolution=' + resolution
1435
- streamURL += content_protect_b
1441
+ streamURL += content_protect_a
1442
+ let multiviewquerystring = streamURL + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1443
+ if ( resolution != VALID_RESOLUTIONS[0] ) streamURL += '&resolution=' + resolution
1436
1444
  if ( linkType != VALID_LINK_TYPES[1] ) {
1437
1445
  streamURL = thislink + '?src=' + encodeURIComponent(streamURL) + '&startFrom=' + VALID_START_FROM[1] + content_protect_b
1438
1446
  }
1439
1447
  body += '<a href="' + streamURL + '">Game Changer</a>'
1440
- body += '<input type="checkbox" value="' + multiviewquerystring + '" onclick="addmultiview(this)">'
1448
+ body += '<input type="checkbox" value="' + multiviewquerystring + '" onclick="addmultiview(this, [], excludeTeams)">'
1441
1449
  } else {
1442
1450
  body += 'Game Changer'
1443
1451
  }
@@ -1733,7 +1741,7 @@ app.get('/', async function(req, res) {
1733
1741
  body += stationlink
1734
1742
  }
1735
1743
  if ( mediaType == 'MLBTV' ) {
1736
- body += '<input type="checkbox" value="' + server + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this)">'
1744
+ body += '<input type="checkbox" value="' + server + '/stream.m3u8' + multiviewquerystring + '" onclick="addmultiview(this, [\'' + awayteam + '\', \'' + hometeam + '\'])">'
1737
1745
  }
1738
1746
  if ( resumeStatus ) {
1739
1747
  body += '('
@@ -2632,4 +2640,4 @@ app.get('/kodi.strm', async function(req, res) {
2632
2640
  session.log('kodi.strm request error : ' + e.message)
2633
2641
  res.end('kodi.strm request error, check log')
2634
2642
  }
2635
- })
2643
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2022.08.08",
3
+ "version": "2022.08.23",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -9,17 +9,8 @@ const readlineSync = require('readline-sync')
9
9
  const FileCookieStore = require('tough-cookie-filestore')
10
10
  const parseString = require('xml2js').parseString
11
11
 
12
- // Define some file paths and names
13
- const DATA_DIRECTORY = path.join(__dirname, 'data')
14
- const CACHE_DIRECTORY = path.join(__dirname, 'cache')
15
12
  const MULTIVIEW_DIRECTORY_NAME = 'multiview'
16
13
 
17
- const CREDENTIALS_FILE = path.join(__dirname, 'credentials.json')
18
- const PROTECTION_FILE = path.join(__dirname, 'protection.json')
19
- const COOKIE_FILE = path.join(DATA_DIRECTORY, 'cookies.json')
20
- const DATA_FILE = path.join(DATA_DIRECTORY, 'data.json')
21
- const CACHE_FILE = path.join(CACHE_DIRECTORY, 'cache.json')
22
-
23
14
  // Default user agent to use for API requests
24
15
  const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:87.0) Gecko/20100101 Firefox/87.0'
25
16
 
@@ -824,8 +815,23 @@ class sessionClass {
824
815
  constructor(argv = {}) {
825
816
  this.debug = argv.debug
826
817
 
818
+ let dirname = __dirname
819
+ if ( argv.data_directory ) {
820
+ dirname = argv.data_directory
821
+ }
822
+
823
+ // Define some file paths and names
824
+ this.DATA_DIRECTORY = path.join(dirname, 'data')
825
+ this.CACHE_DIRECTORY = path.join(dirname, 'cache')
826
+
827
+ this.CREDENTIALS_FILE = path.join(dirname, 'credentials.json')
828
+ this.PROTECTION_FILE = path.join(dirname, 'protection.json')
829
+ this.COOKIE_FILE = path.join(this.DATA_DIRECTORY, 'cookies.json')
830
+ this.DATA_FILE = path.join(this.DATA_DIRECTORY, 'data.json')
831
+ this.CACHE_FILE = path.join(this.CACHE_DIRECTORY, 'cache.json')
832
+
827
833
  // Read credentials from file, if present
828
- this.credentials = this.readFileToJson(CREDENTIALS_FILE) || {}
834
+ this.credentials = this.readFileToJson(this.CREDENTIALS_FILE) || {}
829
835
 
830
836
  // Check if account credentials were provided and if they are different from the stored credentials
831
837
  if ( argv.account_username && argv.account_password && ((argv.account_username != this.credentials.account_username) || (argv.account_password != this.credentials.account_password)) ) {
@@ -850,7 +856,7 @@ class sessionClass {
850
856
  this.protection = {}
851
857
  if ( argv.page_username && argv.page_password ) {
852
858
  // Read protection data from file, if present
853
- this.protection = this.readFileToJson(PROTECTION_FILE) || {}
859
+ this.protection = this.readFileToJson(this.PROTECTION_FILE) || {}
854
860
 
855
861
  // Check if content_protect key was provided and if it is different from the stored one
856
862
  if ( argv.content_protect && (argv.content_protect != this.protection.content_protect) ) {
@@ -869,39 +875,41 @@ class sessionClass {
869
875
  }
870
876
 
871
877
  // Create storage directory if it doesn't already exist
872
- this.createDirectory(DATA_DIRECTORY)
878
+ this.createDirectory(this.DATA_DIRECTORY)
873
879
 
874
880
  // Set multiview path
875
881
  if ( argv.multiview_path ) {
876
- this.multiview_path = path.join(argv.multiview_path, path.basename(__dirname))
882
+ this.multiview_path = path.join(argv.multiview_path, path.basename(dirname))
877
883
  this.createDirectory(this.multiview_path)
878
884
  this.multiview_path = path.join(this.multiview_path, MULTIVIEW_DIRECTORY_NAME)
879
885
  } else {
880
- this.multiview_path = path.join(__dirname, MULTIVIEW_DIRECTORY_NAME)
886
+ this.multiview_path = path.join(dirname, MULTIVIEW_DIRECTORY_NAME)
881
887
  }
882
888
  this.createDirectory(this.multiview_path)
883
889
 
884
890
  // Create cookie storage file if it doesn't already exist
885
- this.createFile(COOKIE_FILE)
891
+ this.createFile(this.COOKIE_FILE)
886
892
  // Verify its contents are valid
887
- let cookieStr = fs.readFileSync(COOKIE_FILE)
893
+ let cookieStr = fs.readFileSync(this.COOKIE_FILE)
888
894
  if ( (cookieStr != '') && !this.isValidJson(cookieStr) ) {
889
895
  this.log('invalid cookie storage file contents, resetting')
890
- fs.unlinkSync(COOKIE_FILE)
891
- this.createFile(COOKIE_FILE)
896
+ fs.unlinkSync(this.COOKIE_FILE)
897
+ this.createFile(this.COOKIE_FILE)
892
898
  }
893
899
 
894
900
  // Set up http requests with the cookie jar
895
901
  this.request = require('request-promise')
896
- this.jar = this.request.jar(new FileCookieStore(COOKIE_FILE))
902
+ this.jar = this.request.jar(new FileCookieStore(this.COOKIE_FILE))
897
903
  this.request = this.request.defaults({timeout:15000, agent:false, jar: this.request.jar()})
898
904
 
899
905
  // Load session data and cache from files
900
- this.data = this.readFileToJson(DATA_FILE) || {}
901
- this.cache = this.readFileToJson(CACHE_FILE) || {}
906
+ this.data = this.readFileToJson(this.DATA_FILE) || {}
907
+ this.cache = this.readFileToJson(this.CACHE_FILE) || {}
902
908
 
903
- // Define empty temporary cache (for skip and gamechanger data)
909
+ // Define empty temporary cache (for skip, gamechanger, and key data)
904
910
  this.temp_cache = {}
911
+ // Store previous keys, for return without retrieval
912
+ this.temp_cache.prevKeys = {}
905
913
 
906
914
  // Default scan_mode and linkType values
907
915
  if ( !this.data.scan_mode ) {
@@ -1222,7 +1230,7 @@ class sessionClass {
1222
1230
 
1223
1231
  logout() {
1224
1232
  try {
1225
- fs.unlinkSync(CREDENTIALS_FILE)
1233
+ fs.unlinkSync(this.CREDENTIALS_FILE)
1226
1234
  } catch(e){
1227
1235
  this.debuglog('credentials cannot be cleared or do not exist yet : ' + e.message)
1228
1236
  }
@@ -1230,8 +1238,8 @@ class sessionClass {
1230
1238
 
1231
1239
  clear_session_data() {
1232
1240
  try {
1233
- fs.unlinkSync(COOKIE_FILE)
1234
- fs.unlinkSync(DATA_FILE)
1241
+ fs.unlinkSync(this.COOKIE_FILE)
1242
+ fs.unlinkSync(this.DATA_FILE)
1235
1243
  } catch(e){
1236
1244
  this.debuglog('session cannot be cleared or does not exist yet : ' + e.message)
1237
1245
  }
@@ -1239,7 +1247,7 @@ class sessionClass {
1239
1247
 
1240
1248
  clear_cache() {
1241
1249
  try {
1242
- fs.unlinkSync(CACHE_FILE)
1250
+ fs.unlinkSync(this.CACHE_FILE)
1243
1251
  } catch(e){
1244
1252
  this.debuglog('cache cannot be cleared or does not exist yet : ' + e.message)
1245
1253
  }
@@ -1268,30 +1276,30 @@ class sessionClass {
1268
1276
  }
1269
1277
 
1270
1278
  save_credentials() {
1271
- this.writeJsonToFile(JSON.stringify(this.credentials), CREDENTIALS_FILE)
1279
+ this.writeJsonToFile(JSON.stringify(this.credentials), this.CREDENTIALS_FILE)
1272
1280
  this.debuglog('credentials saved to file')
1273
1281
  }
1274
1282
 
1275
1283
  save_protection() {
1276
- this.writeJsonToFile(JSON.stringify(this.protection), PROTECTION_FILE)
1284
+ this.writeJsonToFile(JSON.stringify(this.protection), this.PROTECTION_FILE)
1277
1285
  this.debuglog('protection data saved to file')
1278
1286
  }
1279
1287
 
1280
1288
  save_session_data() {
1281
- this.createDirectory(DATA_DIRECTORY)
1282
- this.writeJsonToFile(JSON.stringify(this.data), DATA_FILE)
1289
+ this.createDirectory(this.DATA_DIRECTORY)
1290
+ this.writeJsonToFile(JSON.stringify(this.data), this.DATA_FILE)
1283
1291
  this.debuglog('session data saved to file')
1284
1292
  }
1285
1293
 
1286
1294
  save_cache_data() {
1287
- this.createDirectory(CACHE_DIRECTORY)
1288
- this.writeJsonToFile(JSON.stringify(this.cache), CACHE_FILE)
1295
+ this.createDirectory(this.CACHE_DIRECTORY)
1296
+ this.writeJsonToFile(JSON.stringify(this.cache), this.CACHE_FILE)
1289
1297
  this.debuglog('cache data saved to file')
1290
1298
  }
1291
1299
 
1292
1300
  save_json_cache_file(cache_name, cache_data) {
1293
- this.createDirectory(CACHE_DIRECTORY)
1294
- this.writeJsonToFile(JSON.stringify(cache_data), path.join(CACHE_DIRECTORY, cache_name+'.json'))
1301
+ this.createDirectory(this.CACHE_DIRECTORY)
1302
+ this.writeJsonToFile(JSON.stringify(cache_data), path.join(this.CACHE_DIRECTORY, cache_name+'.json'))
1295
1303
  this.debuglog('cache file saved')
1296
1304
  }
1297
1305
 
@@ -1312,7 +1320,7 @@ class sessionClass {
1312
1320
  }
1313
1321
 
1314
1322
  // Generic http GET request function
1315
- httpGet(reqObj) {
1323
+ httpGet(reqObj, exit=true) {
1316
1324
  reqObj.jar = this.jar
1317
1325
  return new Promise((resolve, reject) => {
1318
1326
  this.request.get(reqObj)
@@ -1322,7 +1330,11 @@ class sessionClass {
1322
1330
  .catch(function(e) {
1323
1331
  console.log('http get failed : ' + e.message)
1324
1332
  console.log(reqObj)
1325
- process.exit(1)
1333
+ if ( exit ) {
1334
+ process.exit(1)
1335
+ } else {
1336
+ resolve(false)
1337
+ }
1326
1338
  })
1327
1339
  })
1328
1340
  }
@@ -1548,7 +1560,7 @@ class sessionClass {
1548
1560
  gzip: true
1549
1561
  }
1550
1562
  var response = await this.httpGet(reqObj)
1551
- if ( this.isValidJson(response) ) {
1563
+ if ( response && this.isValidJson(response) ) {
1552
1564
  this.debuglog('getStreamURL response : ' + response)
1553
1565
  let obj = JSON.parse(response)
1554
1566
  if ( obj.errors && (obj.errors[0] == 'blackout') ) {
@@ -1655,7 +1667,7 @@ class sessionClass {
1655
1667
  gzip: true
1656
1668
  }
1657
1669
  var response = await this.httpGet(reqObj)
1658
- if ( this.isValidJson(response) ) {
1670
+ if ( response && this.isValidJson(response) ) {
1659
1671
  this.debuglog('getDeviceId response : ' + response)
1660
1672
  let obj = JSON.parse(response)
1661
1673
  this.debuglog('getDeviceId : ' + obj.device.id)
@@ -1865,7 +1877,7 @@ class sessionClass {
1865
1877
 
1866
1878
  let cache_data
1867
1879
  let cache_name = gameDate
1868
- let cache_file = path.join(CACHE_DIRECTORY, gameDate+'.json')
1880
+ let cache_file = path.join(this.CACHE_DIRECTORY, gameDate+'.json')
1869
1881
  let currentDate = new Date()
1870
1882
  cache_data = await this.getDayData(gameDate)
1871
1883
 
@@ -1985,7 +1997,7 @@ class sessionClass {
1985
1997
 
1986
1998
  let cache_data
1987
1999
  let cache_name = 'h' + gamePk
1988
- let cache_file = path.join(CACHE_DIRECTORY, cache_name+'.json')
2000
+ let cache_file = path.join(this.CACHE_DIRECTORY, cache_name+'.json')
1989
2001
  let currentDate = new Date()
1990
2002
  if ( !fs.existsSync(cache_file) || !this.cache || !this.cache.highlights || !this.cache.highlights[cache_name] || !this.cache.highlights[cache_name].highlightsCacheExpiry || (currentDate > new Date(this.cache.highlights[cache_name].highlightsCacheExpiry)) ) {
1991
2003
  let reqObj = {
@@ -1998,8 +2010,8 @@ class sessionClass {
1998
2010
  },
1999
2011
  gzip: true
2000
2012
  }
2001
- var response = await this.httpGet(reqObj)
2002
- if ( this.isValidJson(response) ) {
2013
+ var response = await this.httpGet(reqObj, false)
2014
+ if ( response && this.isValidJson(response) ) {
2003
2015
  //this.debuglog(response)
2004
2016
  cache_data = JSON.parse(response)
2005
2017
  this.save_json_cache_file(cache_name, cache_data)
@@ -2081,7 +2093,7 @@ class sessionClass {
2081
2093
  } else {
2082
2094
  this.debuglog('getDayData for date ' + dateString)
2083
2095
  }
2084
- let cache_file = path.join(CACHE_DIRECTORY, cache_name+'.json')
2096
+ let cache_file = path.join(this.CACHE_DIRECTORY, cache_name+'.json')
2085
2097
  let currentDate = new Date()
2086
2098
  if ( !fs.existsSync(cache_file) || !this.cache || !this.cache.dates || !this.cache.dates[cache_name] || !this.cache.dates[cache_name].dateCacheExpiry || (currentDate > new Date(this.cache.dates[cache_name].dateCacheExpiry)) ) {
2087
2099
  let reqObj = {
@@ -2094,8 +2106,8 @@ class sessionClass {
2094
2106
  },
2095
2107
  gzip: true
2096
2108
  }
2097
- var response = await this.httpGet(reqObj)
2098
- if ( this.isValidJson(response) ) {
2109
+ var response = await this.httpGet(reqObj, false)
2110
+ if ( response && this.isValidJson(response) ) {
2099
2111
  //this.debuglog(response)
2100
2112
  cache_data = JSON.parse(response)
2101
2113
  this.save_json_cache_file(cache_name, cache_data)
@@ -2164,7 +2176,7 @@ class sessionClass {
2164
2176
 
2165
2177
  let cache_data
2166
2178
  let cache_name = 'week'
2167
- let cache_file = path.join(CACHE_DIRECTORY, cache_name + '.json')
2179
+ let cache_file = path.join(this.CACHE_DIRECTORY, cache_name + '.json')
2168
2180
  let currentDate = new Date()
2169
2181
  if ( !fs.existsSync(cache_file) || !this.cache || !this.cache.weekCacheExpiry || (currentDate > new Date(this.cache.weekCacheExpiry)) ) {
2170
2182
  let startDate = this.liveDate(utcHours)
@@ -2181,8 +2193,8 @@ class sessionClass {
2181
2193
  },
2182
2194
  gzip: true
2183
2195
  }
2184
- var response = await this.httpGet(reqObj)
2185
- if ( this.isValidJson(response) ) {
2196
+ var response = await this.httpGet(reqObj, false)
2197
+ if ( response && this.isValidJson(response) ) {
2186
2198
  //this.debuglog(response)
2187
2199
  cache_data = JSON.parse(response)
2188
2200
  this.save_json_cache_file(cache_name, cache_data)
@@ -2597,7 +2609,7 @@ class sessionClass {
2597
2609
  // Get image from cache or request
2598
2610
  async getImage(teamId) {
2599
2611
  this.debuglog('getImage ' + teamId)
2600
- let imagePath = path.join(CACHE_DIRECTORY, teamId + '.svg')
2612
+ let imagePath = path.join(this.CACHE_DIRECTORY, teamId + '.svg')
2601
2613
  if ( fs.existsSync(imagePath) ) {
2602
2614
  this.debuglog('using cached image for ' + teamId)
2603
2615
  return fs.readFileSync(imagePath)
@@ -2614,7 +2626,7 @@ class sessionClass {
2614
2626
  'origin': 'https://www.mlb.com'
2615
2627
  }
2616
2628
  }
2617
- var response = await this.httpGet(reqObj)
2629
+ var response = await this.httpGet(reqObj, false)
2618
2630
  if ( response ) {
2619
2631
  this.debuglog('getImage response : ' + response)
2620
2632
  fs.writeFileSync(imagePath, response)
@@ -2641,7 +2653,7 @@ class sessionClass {
2641
2653
  } else {
2642
2654
  this.debuglog('getAiringsData for content ' + contentId)
2643
2655
  }
2644
- let cache_file = path.join(CACHE_DIRECTORY, cache_name+'.json')
2656
+ let cache_file = path.join(this.CACHE_DIRECTORY, cache_name+'.json')
2645
2657
  let currentDate = new Date()
2646
2658
  if ( !fs.existsSync(cache_file) || !this.cache || !this.cache.airings || !this.cache.airings[cache_name] || !this.cache.airings[cache_name].airingsCacheExpiry || (currentDate > new Date(this.cache.airings[cache_name].airingsCacheExpiry)) ) {
2647
2659
  let reqObj = {
@@ -2659,7 +2671,7 @@ class sessionClass {
2659
2671
  gzip: true
2660
2672
  }
2661
2673
  var response = await this.httpGet(reqObj)
2662
- if ( this.isValidJson(response) ) {
2674
+ if ( response && this.isValidJson(response) ) {
2663
2675
  this.debuglog(response)
2664
2676
  cache_data = JSON.parse(response)
2665
2677
  this.save_json_cache_file(cache_name, cache_data)
@@ -2710,7 +2722,7 @@ class sessionClass {
2710
2722
 
2711
2723
  let cache_data
2712
2724
  let cache_name = 'g' + gamePk
2713
- let cache_file = path.join(CACHE_DIRECTORY, cache_name+'.json')
2725
+ let cache_file = path.join(this.CACHE_DIRECTORY, cache_name+'.json')
2714
2726
  let currentDate = new Date()
2715
2727
  if ( !fs.existsSync(cache_file) || !this.cache || !this.cache.gameday || !this.cache.gameday[cache_name] || !this.cache.gameday[cache_name].gamedayCacheExpiry || (currentDate > new Date(this.cache.gameday[cache_name].gamedayCacheExpiry)) ) {
2716
2728
  let reqObj = {
@@ -2723,8 +2735,8 @@ class sessionClass {
2723
2735
  },
2724
2736
  gzip: true
2725
2737
  }
2726
- var response = await this.httpGet(reqObj)
2727
- if ( this.isValidJson(response) ) {
2738
+ var response = await this.httpGet(reqObj, false)
2739
+ if ( response && this.isValidJson(response) ) {
2728
2740
  this.debuglog(response)
2729
2741
  cache_data = JSON.parse(response)
2730
2742
  this.save_json_cache_file(cache_name, cache_data)
@@ -2910,6 +2922,19 @@ class sessionClass {
2910
2922
  continue
2911
2923
  } else {
2912
2924
  let break_end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].startTime) - broadcast_start_timestamp) / 1000) + EVENT_START_PADDING
2925
+
2926
+ // attempt to fix erroneous timestamps, like NYY-SEA 2022-08-09, bottom 11
2927
+ if ( break_end < break_start ) {
2928
+ this.debuglog('getSkipMarkers adjusting break start')
2929
+ break_start = break_end - 10
2930
+
2931
+ let prev_break = skip_markers.length-1
2932
+ if ( (prev_break > 0) && (break_start < skip_markers[prev_break].break_end) && (skip_markers[prev_break].break_start < (skip_markers[prev_break].break_end - 40)) ) {
2933
+ this.debuglog('getSkipMarkers adjusting previous break end')
2934
+ skip_markers[prev_break].break_end = skip_markers[prev_break].break_start + 30
2935
+ }
2936
+ }
2937
+
2913
2938
  // if the break duration should be greater than than our specified minimum
2914
2939
  // and if skip type is not 1 (inning breaks) or the inning has changed
2915
2940
  // then we'll add the skip marker
@@ -2977,7 +3002,7 @@ class sessionClass {
2977
3002
  },
2978
3003
  gzip: true
2979
3004
  }
2980
- var response = await this.httpGet(reqObj)
3005
+ var response = await this.httpGet(reqObj, false)
2981
3006
  if ( response ) {
2982
3007
  // disabled because it's very big!
2983
3008
  //this.debuglog(response)
@@ -3089,11 +3114,12 @@ class sessionClass {
3089
3114
 
3090
3115
  let cache_data
3091
3116
  let cache_name = 'events'
3092
- let cache_file = path.join(CACHE_DIRECTORY, cache_name + '.json')
3117
+ let cache_file = path.join(this.CACHE_DIRECTORY, cache_name + '.json')
3093
3118
  let currentDate = new Date()
3094
3119
  if ( !fs.existsSync(cache_file) || !this.cache || !this.cache.eventURLCacheExpiry || (currentDate > new Date(this.cache.eventURLCacheExpiry)) ) {
3095
3120
  let reqObj = {
3096
- url: 'https://dapi.cms.mlbinfra.com/v2/content/en-us/sel-mlbtv-featured-svod-video-list',
3121
+ //url: 'https://dapi.cms.mlbinfra.com/v2/content/en-us/sel-mlbtv-featured-svod-video-list',
3122
+ url: 'https://dapi.mlbinfra.com/v2/content/en-us/vsmcontents/mlb-tv-welcome-center-big-inning-show',
3097
3123
  headers: {
3098
3124
  'User-Agent': USER_AGENT,
3099
3125
  'Origin': 'https://www.mlb.com',
@@ -3103,8 +3129,8 @@ class sessionClass {
3103
3129
  },
3104
3130
  gzip: true
3105
3131
  }
3106
- var response = await this.httpGet(reqObj)
3107
- if ( this.isValidJson(response) ) {
3132
+ var response = await this.httpGet(reqObj, false)
3133
+ if ( response && this.isValidJson(response) ) {
3108
3134
  this.debuglog(response)
3109
3135
  cache_data = JSON.parse(response)
3110
3136
  this.save_json_cache_file(cache_name, cache_data)
@@ -3140,19 +3166,26 @@ class sessionClass {
3140
3166
 
3141
3167
  let cache_data = await this.getEventData()
3142
3168
 
3169
+ let eventList
3143
3170
  if ( cache_data && cache_data.items ) {
3144
- for (var i=0; i<cache_data.items.length; i++) {
3145
- if ( cache_data.items[i].fields && cache_data.items[i].fields.blurb && cache_data.items[i].fields.url ) {
3146
- if ( !cache_data.items[i].fields.blurb.startsWith('LIVE') ) {
3171
+ eventList = cache_data.items
3172
+ } else if ( cache_data && cache_data.references && cache_data.references.video ) {
3173
+ eventList = cache_data.references.video
3174
+ }
3175
+
3176
+ if ( eventList ) {
3177
+ for (var i=0; i<eventList.length; i++) {
3178
+ if ( eventList[i].fields && eventList[i].fields.blurb && eventList[i].fields.url ) {
3179
+ if ( !eventList[i].fields.blurb.startsWith('LIVE') ) {
3147
3180
  break
3148
3181
  } else {
3149
- if ( cache_data.items[i].title ) {
3150
- if ( (eventName == 'BIGINNING') && cache_data.items[i].title.startsWith('LIVE') && cache_data.items[i].title.includes('Big Inning') ) {
3182
+ if ( eventList[i].title ) {
3183
+ if ( (eventName == 'BIGINNING') && eventList[i].title.startsWith('LIVE') && eventList[i].title.includes('Big Inning') ) {
3151
3184
  this.debuglog('active big inning url')
3152
- return cache_data.items[i].fields.url
3153
- } else if ( cache_data.items[i].title.toUpperCase().endsWith(' VS. ' + eventName) ) {
3185
+ return eventList[i].fields.url
3186
+ } else if ( eventList[i].title.toUpperCase().endsWith(' VS. ' + eventName) ) {
3154
3187
  this.debuglog('active ' + eventName + ' url')
3155
- return cache_data.items[i].fields.url
3188
+ return eventList[i].fields.url
3156
3189
  }
3157
3190
  }
3158
3191
  }
@@ -3190,7 +3223,7 @@ class sessionClass {
3190
3223
  gzip: true
3191
3224
  }
3192
3225
  var response = await this.httpGet(reqObj)
3193
- if ( this.isValidJson(response) ) {
3226
+ if ( response && this.isValidJson(response) ) {
3194
3227
  this.debuglog('getEventStreamURL response : ' + response)
3195
3228
  let obj = JSON.parse(response)
3196
3229
  if ( obj.success && (obj.success == true) ) {
@@ -3227,8 +3260,8 @@ class sessionClass {
3227
3260
  'Referer': 'https://www.mlb.com/'
3228
3261
  }
3229
3262
  }
3230
- var response = await this.httpGet(reqObj)
3231
- if ( this.isValidJson(response) ) {
3263
+ var response = await this.httpGet(reqObj, false)
3264
+ if ( response && this.isValidJson(response) ) {
3232
3265
  this.debuglog('getBlackoutTeams response : ' + response)
3233
3266
  let obj = JSON.parse(response)
3234
3267
  if ( obj.teams ) {
@@ -3380,7 +3413,7 @@ class sessionClass {
3380
3413
  try {
3381
3414
  let cache_data
3382
3415
  let cache_name = 'gamechanger'
3383
- let cache_file = path.join(CACHE_DIRECTORY, cache_name + '.json')
3416
+ let cache_file = path.join(this.CACHE_DIRECTORY, cache_name + '.json')
3384
3417
  let currentDate = new Date()
3385
3418
  if ( !this.temp_cache.gamechanger.cache_data || !this.temp_cache.gamechangerCacheExpiry || (currentDate > new Date(this.temp_cache.gamechangerCacheExpiry)) ) {
3386
3419
  this.debuglog(game_changer_title + 'fetching new gamechanger data')
@@ -3398,8 +3431,8 @@ class sessionClass {
3398
3431
  },
3399
3432
  gzip: true
3400
3433
  }
3401
- var response = await this.httpGet(reqObj)
3402
- if ( this.isValidJson(response) ) {
3434
+ var response = await this.httpGet(reqObj, false)
3435
+ if ( response && this.isValidJson(response) ) {
3403
3436
  this.debuglog(game_changer_title + 'valid json response')
3404
3437
  this.debuglog(response)
3405
3438
  this.temp_cache.gamechanger.cache_data = JSON.parse(response)