mlbserver 2021.10.0-1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/index.js +1837 -0
- package/package.json +30 -0
- package/session.js +2263 -0
package/session.js
ADDED
|
@@ -0,0 +1,2263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// session.js defines the session class which handles API activity
|
|
4
|
+
|
|
5
|
+
// Required Node packages for the session class
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const readlineSync = require('readline-sync')
|
|
9
|
+
const FileCookieStore = require('tough-cookie-filestore')
|
|
10
|
+
const parseString = require('xml2js').parseString
|
|
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
|
+
const MULTIVIEW_DIRECTORY_NAME = 'multiview'
|
|
16
|
+
|
|
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
|
+
// Default user agent to use for API requests
|
|
24
|
+
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:87.0) Gecko/20100101 Firefox/87.0'
|
|
25
|
+
|
|
26
|
+
// Other variables to use in API communications
|
|
27
|
+
const PLATFORM = "macintosh"
|
|
28
|
+
const BAM_SDK_VERSION = '4.3'
|
|
29
|
+
const BAM_TOKEN_URL = 'https://us.edge.bamgrid.com/token'
|
|
30
|
+
|
|
31
|
+
// Default date handling
|
|
32
|
+
const TODAY_UTC_HOURS = 8 // UTC hours (EST + 4) into tomorrow to still use today's date
|
|
33
|
+
|
|
34
|
+
const TEAM_IDS = {'ARI':'109','ATL':'144','BAL':'110','BOS':'111','CHC':'112','CWS':'145','CIN':'113','CLE':'114','COL':'115','DET':'116','HOU':'117','KCR':'118','LAA':'108','LAD':'119','MIA':'146','MIL':'158','MIN':'142','NYM':'121','NYY':'147','OAK':'133','PHI':'143','PIT':'134','STL':'138','SDP':'135','SFG':'137','SEA':'136','TBR':'139','TEX':'140','TOR':'141','WSH':'120'}
|
|
35
|
+
|
|
36
|
+
class sessionClass {
|
|
37
|
+
// Initialize the class
|
|
38
|
+
constructor(argv = {}) {
|
|
39
|
+
this.debug = argv.debug
|
|
40
|
+
|
|
41
|
+
// Read credentials from file, if present
|
|
42
|
+
this.credentials = this.readFileToJson(CREDENTIALS_FILE) || {}
|
|
43
|
+
|
|
44
|
+
// Check if account credentials were provided and if they are different from the stored credentials
|
|
45
|
+
if ( argv.account_username && argv.account_password && ((argv.account_username != this.credentials.account_username) || (argv.account_password != this.credentials.account_password)) ) {
|
|
46
|
+
this.debuglog('updating account credentials')
|
|
47
|
+
this.credentials.account_username = argv.account_username
|
|
48
|
+
this.credentials.account_password = argv.account_password
|
|
49
|
+
this.save_credentials()
|
|
50
|
+
this.clear_session_data()
|
|
51
|
+
} else {
|
|
52
|
+
// Prompt for credentials if they don't exist
|
|
53
|
+
if ( !this.credentials.account_username || !this.credentials.account_password ) {
|
|
54
|
+
this.debuglog('prompting for account credentials')
|
|
55
|
+
this.credentials.account_username = readlineSync.question('Enter account username (email address): ')
|
|
56
|
+
this.credentials.account_password = readlineSync.question('Enter account password: ', { hideEchoBack: true })
|
|
57
|
+
this.save_credentials()
|
|
58
|
+
this.clear_session_data()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If page username/password protection is specified, retrieve or generate a random string of random length
|
|
63
|
+
// to protect non-page content (streams, playlists, guides, images)
|
|
64
|
+
this.protection = {}
|
|
65
|
+
if ( argv.page_username && argv.page_password ) {
|
|
66
|
+
// Read protection data from file, if present
|
|
67
|
+
this.protection = this.readFileToJson(PROTECTION_FILE) || {}
|
|
68
|
+
|
|
69
|
+
if ( !this.protection.content_protect ) {
|
|
70
|
+
this.log('generating new content protection value')
|
|
71
|
+
this.log('** YOU WILL NEED TO UPDATE ANY CONTENT URLS YOU HAVE COPIED OUTSIDE OF MLBSERVER **')
|
|
72
|
+
this.protection.content_protect = this.getRandomString(this.getRandomInteger(32,64))
|
|
73
|
+
this.save_protection()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create storage directories if they don't already exist
|
|
78
|
+
this.createDirectory(DATA_DIRECTORY)
|
|
79
|
+
this.createFile(COOKIE_FILE)
|
|
80
|
+
|
|
81
|
+
// Set multiview path
|
|
82
|
+
if ( argv.multiview_path ) {
|
|
83
|
+
this.multiview_path = path.join(argv.multiview_path, path.basename(__dirname))
|
|
84
|
+
this.createDirectory(this.multiview_path)
|
|
85
|
+
this.multiview_path = path.join(this.multiview_path, MULTIVIEW_DIRECTORY_NAME)
|
|
86
|
+
} else {
|
|
87
|
+
this.multiview_path = path.join(__dirname, MULTIVIEW_DIRECTORY_NAME)
|
|
88
|
+
}
|
|
89
|
+
this.createDirectory(this.multiview_path)
|
|
90
|
+
|
|
91
|
+
// Set up http requests with the cookie jar
|
|
92
|
+
this.request = require('request-promise')
|
|
93
|
+
this.jar = this.request.jar(new FileCookieStore(COOKIE_FILE))
|
|
94
|
+
this.request = this.request.defaults({timeout:15000, agent:false, jar: this.request.jar()})
|
|
95
|
+
|
|
96
|
+
// Load session data and cache from files
|
|
97
|
+
this.data = this.readFileToJson(DATA_FILE) || {}
|
|
98
|
+
this.cache = this.readFileToJson(CACHE_FILE) || {}
|
|
99
|
+
|
|
100
|
+
// Define empty temporary cache (for inning data)
|
|
101
|
+
this.temp_cache = {}
|
|
102
|
+
|
|
103
|
+
// Default scan_mode and linkType values
|
|
104
|
+
if ( !this.data.scan_mode ) {
|
|
105
|
+
this.setScanMode('on')
|
|
106
|
+
}
|
|
107
|
+
if ( !this.data.linkType ) {
|
|
108
|
+
this.setLinkType('embed')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Store the ports, used for generating URLs
|
|
113
|
+
setPorts(port, multiviewPort) {
|
|
114
|
+
this.data.port = port
|
|
115
|
+
this.data.multiviewPort = multiviewPort
|
|
116
|
+
this.save_session_data()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Set the scan_mode
|
|
120
|
+
// "on" will return the sample stream for all live channels.m3u stream requests
|
|
121
|
+
setScanMode(x) {
|
|
122
|
+
this.log('scan_mode set to ' + x)
|
|
123
|
+
this.data.scan_mode = x
|
|
124
|
+
this.save_session_data()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Set the linkType
|
|
128
|
+
// used for storing the desired page type across throughout site navigation
|
|
129
|
+
setLinkType(x) {
|
|
130
|
+
this.data.linkType = x
|
|
131
|
+
this.save_session_data()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Set the multiview stream URL path
|
|
135
|
+
setMultiviewStreamURLPath(url_path) {
|
|
136
|
+
this.data.multiviewStreamURLPath = url_path
|
|
137
|
+
this.save_session_data()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Some basic self-explanatory functions
|
|
141
|
+
createDirectory(directoryPath) {
|
|
142
|
+
if (fs.existsSync(directoryPath) && !fs.lstatSync(directoryPath).isDirectory() ){
|
|
143
|
+
fs.unlinkSync(directoryPath);
|
|
144
|
+
}
|
|
145
|
+
if (!fs.existsSync(directoryPath)){
|
|
146
|
+
fs.mkdirSync(directoryPath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
createFile(filePath) {
|
|
151
|
+
if (!fs.existsSync(filePath)) {
|
|
152
|
+
fs.closeSync(fs.openSync(filePath, 'w'))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
isValidJson(str) {
|
|
157
|
+
try {
|
|
158
|
+
JSON.parse(str);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
readFileToJson(filePath) {
|
|
166
|
+
if (fs.existsSync(filePath)) {
|
|
167
|
+
return JSON.parse(fs.readFileSync(filePath))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
writeJsonToFile(jsonStr, filePath) {
|
|
172
|
+
if (this.isValidJson(jsonStr)) {
|
|
173
|
+
fs.writeFileSync(filePath, jsonStr)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
checkValidItem(item, obj) {
|
|
178
|
+
if (obj.includes(item)) {
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
returnValidItem(item, obj) {
|
|
185
|
+
if (!obj.includes(item)) return obj[0]
|
|
186
|
+
else return item
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
sortObj(obj) {
|
|
190
|
+
return Object.keys(obj).sort().reduce(function (result, key) {
|
|
191
|
+
result[key] = obj[key];
|
|
192
|
+
return result;
|
|
193
|
+
}, {});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
localTimeString() {
|
|
197
|
+
let curDate = new Date()
|
|
198
|
+
return curDate.toLocaleString()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getTodayUTCHours() {
|
|
202
|
+
return TODAY_UTC_HOURS
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
getUserAgent() {
|
|
206
|
+
return USER_AGENT
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// the live date is today's date, or if before a specified hour (UTC time), then use yesterday's date
|
|
210
|
+
liveDate(hour = TODAY_UTC_HOURS) {
|
|
211
|
+
let curDate = new Date()
|
|
212
|
+
if ( curDate.getUTCHours() < hour ) {
|
|
213
|
+
curDate.setDate(curDate.getDate()-1)
|
|
214
|
+
}
|
|
215
|
+
return curDate.toISOString().substring(0,10)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
yesterdayDate() {
|
|
219
|
+
let curDate = new Date(this.liveDate())
|
|
220
|
+
curDate.setDate(curDate.getDate()-1)
|
|
221
|
+
return curDate.toISOString().substring(0,10)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
convertDateToXMLTV(x) {
|
|
225
|
+
let newDate = String(x.getFullYear()) + String(x.getMonth() + 1).padStart(2, '0') + String(x.getDate()).padStart(2, '0') + String(x.getHours()).padStart(2, '0') + String(x.getMinutes()).padStart(2, '0') + String(x.getSeconds()).padStart(2, '0') + " "
|
|
226
|
+
let offset = x.getTimezoneOffset()
|
|
227
|
+
if ( offset > 0 ) {
|
|
228
|
+
newDate += "-"
|
|
229
|
+
} else {
|
|
230
|
+
newDate += "+"
|
|
231
|
+
}
|
|
232
|
+
newDate += String((offset / 60)).padStart(2, '0') + "00"
|
|
233
|
+
return newDate
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getCacheUpdatedDate(dateString) {
|
|
237
|
+
return this.cache.dates[dateString].updated
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
setHighlightsCacheExpiry(cache_name, expiryDate) {
|
|
241
|
+
if ( !this.cache.highlights ) {
|
|
242
|
+
this.cache.highlights={}
|
|
243
|
+
}
|
|
244
|
+
if ( !this.cache.highlights[cache_name] ) {
|
|
245
|
+
this.cache.highlights[cache_name] = {}
|
|
246
|
+
}
|
|
247
|
+
this.cache.highlights[cache_name].highlightsCacheExpiry = expiryDate
|
|
248
|
+
this.save_cache_data()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
setDateCacheExpiry(cache_name, expiryDate) {
|
|
252
|
+
if ( !this.cache.dates ) {
|
|
253
|
+
this.cache.dates={}
|
|
254
|
+
}
|
|
255
|
+
if ( !this.cache.dates[cache_name] ) {
|
|
256
|
+
this.cache.dates[cache_name] = {}
|
|
257
|
+
}
|
|
258
|
+
this.cache.dates[cache_name].dateCacheExpiry = expiryDate
|
|
259
|
+
this.cache.dates[cache_name].updated = this.localTimeString()
|
|
260
|
+
this.save_cache_data()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
setAiringsCacheExpiry(cache_name, expiryDate) {
|
|
264
|
+
if ( !this.cache.airings ) {
|
|
265
|
+
this.cache.airings={}
|
|
266
|
+
}
|
|
267
|
+
if ( !this.cache.airings[cache_name] ) {
|
|
268
|
+
this.cache.airings[cache_name] = {}
|
|
269
|
+
}
|
|
270
|
+
this.cache.airings[cache_name].airingsCacheExpiry = expiryDate
|
|
271
|
+
this.save_cache_data()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
setGamedayCacheExpiry(cache_name, expiryDate) {
|
|
275
|
+
if ( !this.cache.gameday ) {
|
|
276
|
+
this.cache.gameday={}
|
|
277
|
+
}
|
|
278
|
+
if ( !this.cache.gameday[cache_name] ) {
|
|
279
|
+
this.cache.gameday[cache_name] = {}
|
|
280
|
+
}
|
|
281
|
+
this.cache.gameday[cache_name].gamedayCacheExpiry = expiryDate
|
|
282
|
+
this.save_cache_data()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
createContentCache(contentId) {
|
|
286
|
+
if ( !this.cache.content ) {
|
|
287
|
+
this.cache.content = {}
|
|
288
|
+
}
|
|
289
|
+
if ( !this.cache.content[contentId] ) {
|
|
290
|
+
this.cache.content[contentId] = {}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
createMediaCache(mediaId) {
|
|
295
|
+
if ( !this.cache.media ) {
|
|
296
|
+
this.cache.media = {}
|
|
297
|
+
}
|
|
298
|
+
if ( !this.cache.media[mediaId] ) {
|
|
299
|
+
this.cache.media[mediaId] = {}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
cacheMediaId(contentId, mediaId) {
|
|
304
|
+
this.createContentCache(contentId)
|
|
305
|
+
this.cache.content[contentId].mediaId = mediaId
|
|
306
|
+
this.save_cache_data()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
cacheGamePk(contentId, gamePk) {
|
|
310
|
+
this.createContentCache(contentId)
|
|
311
|
+
this.cache.content[contentId].gamePk = gamePk
|
|
312
|
+
this.save_cache_data()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
cacheStreamURL(mediaId, streamURL) {
|
|
316
|
+
this.createMediaCache(mediaId)
|
|
317
|
+
this.cache.media[mediaId].streamURL = streamURL
|
|
318
|
+
// Expire it in 1 minute
|
|
319
|
+
let seconds_to_expire = 60
|
|
320
|
+
this.cache.media[mediaId].streamURLExpiry = new Date(new Date().getTime() + seconds_to_expire * 1000)
|
|
321
|
+
this.save_cache_data()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
markBlackoutError(mediaId) {
|
|
325
|
+
this.createMediaCache(mediaId)
|
|
326
|
+
this.log('saving blackout error to prevent repeated access attempts')
|
|
327
|
+
this.cache.media[mediaId].blackout = true
|
|
328
|
+
// Expire it in 1 hour
|
|
329
|
+
let seconds_to_expire = 60*60
|
|
330
|
+
this.cache.media[mediaId].blackoutExpiry = new Date(new Date().getTime() + seconds_to_expire * 1000)
|
|
331
|
+
this.save_cache_data()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
log(msg) {
|
|
335
|
+
console.log(this.localTimeString() + ' ' + msg)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
debuglog(msg) {
|
|
339
|
+
if (this.debug) this.log(msg)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
halt(msg) {
|
|
343
|
+
this.log(msg)
|
|
344
|
+
process.exit(1)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
logout() {
|
|
348
|
+
try {
|
|
349
|
+
fs.unlinkSync(CREDENTIALS_FILE)
|
|
350
|
+
} catch(e){
|
|
351
|
+
this.debuglog('credentials cannot be cleared or do not exist yet : ' + e.message)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
clear_session_data() {
|
|
356
|
+
try {
|
|
357
|
+
fs.unlinkSync(COOKIE_FILE)
|
|
358
|
+
fs.unlinkSync(DATA_FILE)
|
|
359
|
+
} catch(e){
|
|
360
|
+
this.debuglog('session cannot be cleared or does not exist yet : ' + e.message)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
clear_cache() {
|
|
365
|
+
try {
|
|
366
|
+
fs.unlinkSync(CACHE_FILE)
|
|
367
|
+
} catch(e){
|
|
368
|
+
this.debuglog('cache cannot be cleared or does not exist yet : ' + e.message)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
get_multiview_directory() {
|
|
373
|
+
return this.multiview_path
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
clear_multiview_files() {
|
|
377
|
+
try {
|
|
378
|
+
if ( this.multiview_path ) {
|
|
379
|
+
fs.rmdir(this.multiview_path, { recursive: true }, (err) => {
|
|
380
|
+
if (err) throw err;
|
|
381
|
+
|
|
382
|
+
this.createDirectory(this.multiview_path)
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
} catch(e){
|
|
386
|
+
this.debuglog('recursive clear multiview files error: ' + e.message)
|
|
387
|
+
try {
|
|
388
|
+
if ( this.multiview_path ) {
|
|
389
|
+
fs.readdir(this.multiview_path, (err, files) => {
|
|
390
|
+
if (err) throw err
|
|
391
|
+
|
|
392
|
+
for (const file of files) {
|
|
393
|
+
fs.unlink(path.join(this.multiview_path, file), err => {
|
|
394
|
+
if (err) throw err
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
} catch(e){
|
|
400
|
+
this.debuglog('clear multiview files error : ' + e.message)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
save_credentials() {
|
|
406
|
+
this.writeJsonToFile(JSON.stringify(this.credentials), CREDENTIALS_FILE)
|
|
407
|
+
this.debuglog('credentials saved to file')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
save_protection() {
|
|
411
|
+
this.writeJsonToFile(JSON.stringify(this.protection), PROTECTION_FILE)
|
|
412
|
+
this.debuglog('protection data saved to file')
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
save_session_data() {
|
|
416
|
+
this.createDirectory(DATA_DIRECTORY)
|
|
417
|
+
this.writeJsonToFile(JSON.stringify(this.data), DATA_FILE)
|
|
418
|
+
this.debuglog('session data saved to file')
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
save_cache_data() {
|
|
422
|
+
this.createDirectory(CACHE_DIRECTORY)
|
|
423
|
+
this.writeJsonToFile(JSON.stringify(this.cache), CACHE_FILE)
|
|
424
|
+
this.debuglog('cache data saved to file')
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
save_json_cache_file(cache_name, cache_data) {
|
|
428
|
+
this.createDirectory(CACHE_DIRECTORY)
|
|
429
|
+
this.writeJsonToFile(JSON.stringify(cache_data), path.join(CACHE_DIRECTORY, cache_name+'.json'))
|
|
430
|
+
this.debuglog('cache file saved')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Generate a random integer in a range
|
|
434
|
+
getRandomInteger(min, max) {
|
|
435
|
+
return Math.floor(Math.random() * (max - min) ) + min;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Generate a random string of specified length
|
|
439
|
+
getRandomString(length) {
|
|
440
|
+
var s = ''
|
|
441
|
+
do {
|
|
442
|
+
s += Math.random().toString(36).substr(2);
|
|
443
|
+
} while (s.length < length)
|
|
444
|
+
s = s.substr(0, length)
|
|
445
|
+
|
|
446
|
+
return s
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Generic http GET request function
|
|
450
|
+
httpGet(reqObj) {
|
|
451
|
+
reqObj.jar = this.jar
|
|
452
|
+
return new Promise((resolve, reject) => {
|
|
453
|
+
this.request.get(reqObj)
|
|
454
|
+
.then(function(body) {
|
|
455
|
+
resolve(body)
|
|
456
|
+
})
|
|
457
|
+
.catch(function(e) {
|
|
458
|
+
console.error('http get failed : ' + e.message)
|
|
459
|
+
console.error(reqObj)
|
|
460
|
+
process.exit(1)
|
|
461
|
+
})
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Generic http POST request function
|
|
466
|
+
httpPost(reqObj) {
|
|
467
|
+
reqObj.jar = this.jar
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
this.request.post(reqObj)
|
|
470
|
+
.then(function(body) {
|
|
471
|
+
resolve(body)
|
|
472
|
+
})
|
|
473
|
+
.catch(function(e) {
|
|
474
|
+
console.error('http post failed : ' + e.message)
|
|
475
|
+
console.error(reqObj)
|
|
476
|
+
process.exit(1)
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// request to use when fetching videos
|
|
482
|
+
streamVideo(u, opts, tries, cb) {
|
|
483
|
+
opts.jar = this.jar
|
|
484
|
+
opts.headers = {
|
|
485
|
+
'Authorization': this.data.bamAccessToken,
|
|
486
|
+
'User-Agent': USER_AGENT
|
|
487
|
+
}
|
|
488
|
+
this.request(u, opts, cb)
|
|
489
|
+
.catch(function(e) {
|
|
490
|
+
console.error('stream video failed : ' + e.message)
|
|
491
|
+
console.error(u)
|
|
492
|
+
if ( tries == 1 ) process.exit(1)
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// request to use when fetching audio playlist URL
|
|
497
|
+
async getAudioPlaylistURL(url) {
|
|
498
|
+
var playlistURL
|
|
499
|
+
let reqObj = {
|
|
500
|
+
url: url,
|
|
501
|
+
headers: {
|
|
502
|
+
'Authorization': this.data.bamAccessToken,
|
|
503
|
+
'User-Agent': USER_AGENT
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
var response = await this.httpGet(reqObj)
|
|
507
|
+
var body = response.toString().trim().split('\n')
|
|
508
|
+
for (var i=0; i<body.length; i++) {
|
|
509
|
+
if ( body[i][0] != '#' ) {
|
|
510
|
+
playlistURL = body[i]
|
|
511
|
+
break
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if ( playlistURL ) {
|
|
515
|
+
return playlistURL
|
|
516
|
+
} else {
|
|
517
|
+
session.log('Failed to find audio playlist URL from ' + url)
|
|
518
|
+
return ''
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async getXApiKey() {
|
|
523
|
+
this.debuglog('getXApiKey')
|
|
524
|
+
if ( !this.data.xApiKey || !this.data.xApiKey ) {
|
|
525
|
+
await this.getApiKeys()
|
|
526
|
+
if ( this.data.xApiKey ) return this.data.xApiKey
|
|
527
|
+
} else {
|
|
528
|
+
return this.data.xApiKey
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async getClientApiKey() {
|
|
533
|
+
this.debuglog('getClientApiKey')
|
|
534
|
+
if ( !this.data.clientApiKey ) {
|
|
535
|
+
await this.getApiKeys()
|
|
536
|
+
if ( this.data.clientApiKey ) return this.data.clientApiKey
|
|
537
|
+
} else {
|
|
538
|
+
return this.data.clientApiKey
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// API call
|
|
543
|
+
async getApiKeys() {
|
|
544
|
+
this.debuglog('getApiKeys')
|
|
545
|
+
let reqObj = {
|
|
546
|
+
url: 'https://www.mlb.com/tv/g632102/',
|
|
547
|
+
headers: {
|
|
548
|
+
'User-agent': USER_AGENT,
|
|
549
|
+
'Origin': 'https://www.mlb.com',
|
|
550
|
+
'Accept-Encoding': 'gzip, deflate, br'
|
|
551
|
+
},
|
|
552
|
+
gzip: true
|
|
553
|
+
}
|
|
554
|
+
var response = await this.httpGet(reqObj)
|
|
555
|
+
// disabled because it's very big!
|
|
556
|
+
//this.debuglog('getApiKeys response : ' + response)
|
|
557
|
+
var parsed = response.match('"x-api-key","value":"([^"]+)"')
|
|
558
|
+
if ( parsed[1] ) {
|
|
559
|
+
this.data.xApiKey = parsed[1]
|
|
560
|
+
this.save_session_data()
|
|
561
|
+
}
|
|
562
|
+
parsed = response.match('"clientApiKey":"([^"]+)"')
|
|
563
|
+
if ( parsed[1] ) {
|
|
564
|
+
this.data.clientApiKey = parsed[1]
|
|
565
|
+
this.save_session_data()
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// API call
|
|
570
|
+
async getOktaClientId() {
|
|
571
|
+
this.debuglog('getOktaClientId')
|
|
572
|
+
if ( !this.data.oktaClientId ) {
|
|
573
|
+
this.debuglog('need to get oktaClientId')
|
|
574
|
+
let reqObj = {
|
|
575
|
+
url: 'https://www.mlbstatic.com/mlb.com/vendor/mlb-okta/mlb-okta.js',
|
|
576
|
+
headers: {
|
|
577
|
+
'User-agent': USER_AGENT,
|
|
578
|
+
'Origin': 'https://www.mlb.com',
|
|
579
|
+
'Accept-Encoding': 'gzip, deflate, br'
|
|
580
|
+
},
|
|
581
|
+
gzip: true
|
|
582
|
+
}
|
|
583
|
+
var response = await this.httpGet(reqObj)
|
|
584
|
+
// disabled because it's very big!
|
|
585
|
+
//this.debuglog('getOktaClientId response : ' + response)
|
|
586
|
+
var parsed = response.match('production:{clientId:"([^"]+)",')
|
|
587
|
+
if ( parsed[1] ) {
|
|
588
|
+
this.data.oktaClientId = parsed[1]
|
|
589
|
+
this.save_session_data()
|
|
590
|
+
return this.data.oktaClientId
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
return this.data.oktaClientId
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// API call
|
|
598
|
+
async getMediaIdFromContentId(contentId) {
|
|
599
|
+
this.debuglog('getMediaIdFromContentId from ' + contentId)
|
|
600
|
+
if ( this.cache.content && this.cache.content[contentId] && this.cache.content[contentId].mediaId ) {
|
|
601
|
+
this.debuglog('using cached mediaId')
|
|
602
|
+
return this.cache.content[contentId].mediaId
|
|
603
|
+
} else {
|
|
604
|
+
let cache_data = await this.getAiringsData(contentId)
|
|
605
|
+
let mediaId = cache_data.data.Airings[0].mediaId
|
|
606
|
+
this.cacheMediaId(contentId, mediaId)
|
|
607
|
+
return mediaId
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// API call
|
|
612
|
+
async getGamePkFromContentId(contentId) {
|
|
613
|
+
this.debuglog('getGamePkFromContentId from ' + contentId)
|
|
614
|
+
if ( this.cache.content && this.cache.content[contentId] && this.cache.content[contentId].gamePk ) {
|
|
615
|
+
this.debuglog('using cached gamePk')
|
|
616
|
+
return this.cache.content[contentId].gamePk
|
|
617
|
+
} else {
|
|
618
|
+
let cache_data = await this.getAiringsData(contentId)
|
|
619
|
+
let gamePk = cache_data.data.Airings[0].partnerProgramId
|
|
620
|
+
this.cacheGamePk(contentId, gamePk)
|
|
621
|
+
return gamePk
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// API call
|
|
626
|
+
async getStreamURL(mediaId) {
|
|
627
|
+
this.debuglog('getStreamURL from ' + mediaId)
|
|
628
|
+
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()) ) {
|
|
629
|
+
this.debuglog('using cached streamURL')
|
|
630
|
+
return this.cache.media[mediaId].streamURL
|
|
631
|
+
} 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()) ) {
|
|
632
|
+
this.log('mediaId recently blacked out, skipping')
|
|
633
|
+
} else {
|
|
634
|
+
let playbackURL = 'https://edge.svcs.mlb.com/media/' + mediaId + '/scenarios/browser~csai'
|
|
635
|
+
let reqObj = {
|
|
636
|
+
url: playbackURL,
|
|
637
|
+
simple: false,
|
|
638
|
+
headers: {
|
|
639
|
+
'Authorization': await this.getBamAccessToken() || this.halt('missing bamAccessToken'),
|
|
640
|
+
'User-agent': USER_AGENT,
|
|
641
|
+
'Accept': 'application/vnd.media-service+json; version=1',
|
|
642
|
+
'x-bamsdk-version': BAM_SDK_VERSION,
|
|
643
|
+
'x-bamsdk-platform': PLATFORM,
|
|
644
|
+
'Origin': 'https://www.mlb.com',
|
|
645
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
646
|
+
'Content-type': 'application/json'
|
|
647
|
+
},
|
|
648
|
+
gzip: true
|
|
649
|
+
}
|
|
650
|
+
var response = await this.httpGet(reqObj)
|
|
651
|
+
if ( this.isValidJson(response) ) {
|
|
652
|
+
this.debuglog('getStreamURL response : ' + response)
|
|
653
|
+
let obj = JSON.parse(response)
|
|
654
|
+
if ( obj.errors && (obj.errors[0] == 'blackout') ) {
|
|
655
|
+
this.log('blackout error')
|
|
656
|
+
this.markBlackoutError(mediaId)
|
|
657
|
+
} else {
|
|
658
|
+
this.debuglog('getStreamURL : ' + obj.stream.complete)
|
|
659
|
+
this.cacheStreamURL(mediaId, obj.stream.complete)
|
|
660
|
+
return obj.stream.complete
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// API call
|
|
667
|
+
async getBamAccessToken() {
|
|
668
|
+
this.debuglog('getBamAccessToken')
|
|
669
|
+
if ( !this.data.bamAccessToken || !this.data.bamAccessTokenExpiry || (Date.parse(this.data.bamAccessTokenExpiry) < new Date()) ) {
|
|
670
|
+
this.debuglog('need to get new bamAccessToken')
|
|
671
|
+
let reqObj = {
|
|
672
|
+
url: BAM_TOKEN_URL,
|
|
673
|
+
headers: {
|
|
674
|
+
'Authorization': 'Bearer ' + await this.getClientApiKey() || this.halt('missing clientApiKey'),
|
|
675
|
+
'User-agent': USER_AGENT,
|
|
676
|
+
'Accept': 'application/vnd.media-service+json; version=1',
|
|
677
|
+
'x-bamsdk-version': BAM_SDK_VERSION,
|
|
678
|
+
'x-bamsdk-platform': PLATFORM,
|
|
679
|
+
'Origin': 'https://www.mlb.com',
|
|
680
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
681
|
+
'Content-type': 'application/json'
|
|
682
|
+
},
|
|
683
|
+
form: {
|
|
684
|
+
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
|
|
685
|
+
'platform': 'browser',
|
|
686
|
+
'subject_token': await this.getEntitlementToken() || this.halt('missing EntitlementToken'),
|
|
687
|
+
'subject_token_type': 'urn:bamtech:params:oauth:token-type:account'
|
|
688
|
+
},
|
|
689
|
+
gzip: true
|
|
690
|
+
}
|
|
691
|
+
var response = await this.httpPost(reqObj)
|
|
692
|
+
if ( this.isValidJson(response) ) {
|
|
693
|
+
let obj = JSON.parse(response)
|
|
694
|
+
this.debuglog('getBamAccessToken : ' + obj.access_token)
|
|
695
|
+
this.debuglog('getBamAccessToken expires in : ' + obj.expires_in)
|
|
696
|
+
this.data.bamAccessToken = obj.access_token
|
|
697
|
+
this.data.bamAccessTokenExpiry = new Date(new Date().getTime() + obj.expires_in * 1000)
|
|
698
|
+
this.save_session_data()
|
|
699
|
+
return this.data.bamAccessToken
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
return this.data.bamAccessToken
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// API call
|
|
707
|
+
async getEntitlementToken() {
|
|
708
|
+
this.debuglog('getEntitlementToken')
|
|
709
|
+
let reqObj = {
|
|
710
|
+
url: 'https://media-entitlement.mlb.com/api/v3/jwt',
|
|
711
|
+
headers: {
|
|
712
|
+
'Authorization': 'Bearer ' + await this.getOktaAccessToken() || this.halt('missing OktaAccessToken'),
|
|
713
|
+
'Origin': 'https://www.mlb.com',
|
|
714
|
+
'x-api-key': await this.getXApiKey() || this.halt('missing xApiKey'),
|
|
715
|
+
'Accept-Encoding': 'gzip, deflate, br'
|
|
716
|
+
},
|
|
717
|
+
qs: {
|
|
718
|
+
'os': PLATFORM,
|
|
719
|
+
'did': await this.getDeviceId() || this.halt('missing deviceId'),
|
|
720
|
+
'appname': 'mlbtv_web'
|
|
721
|
+
},
|
|
722
|
+
gzip: true
|
|
723
|
+
}
|
|
724
|
+
var response = await this.httpGet(reqObj)
|
|
725
|
+
this.debuglog('getEntitlementToken response : ' + response)
|
|
726
|
+
this.debuglog('getEntitlementToken : ' + response)
|
|
727
|
+
return response
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async getDeviceId() {
|
|
731
|
+
this.debuglog('getDeviceId')
|
|
732
|
+
let reqObj = {
|
|
733
|
+
url: 'https://us.edge.bamgrid.com/session',
|
|
734
|
+
headers: {
|
|
735
|
+
'Authorization': await this.getDeviceAccessToken() || this.halt('missing device_access_token'),
|
|
736
|
+
'User-agent': USER_AGENT,
|
|
737
|
+
'Origin': 'https://www.mlb.com',
|
|
738
|
+
'Accept': 'application/vnd.session-service+json; version=1',
|
|
739
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
740
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
741
|
+
'x-bamsdk-version': BAM_SDK_VERSION,
|
|
742
|
+
'x-bamsdk-platform': PLATFORM,
|
|
743
|
+
'Content-type': 'application/json',
|
|
744
|
+
'TE': 'Trailers'
|
|
745
|
+
},
|
|
746
|
+
gzip: true
|
|
747
|
+
}
|
|
748
|
+
var response = await this.httpGet(reqObj)
|
|
749
|
+
if ( this.isValidJson(response) ) {
|
|
750
|
+
this.debuglog('getDeviceId response : ' + response)
|
|
751
|
+
let obj = JSON.parse(response)
|
|
752
|
+
this.debuglog('getDeviceId : ' + obj.device.id)
|
|
753
|
+
return obj.device.id
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// API call
|
|
758
|
+
async getDeviceAccessToken() {
|
|
759
|
+
this.debuglog('getDeviceAccessToken')
|
|
760
|
+
let reqObj = {
|
|
761
|
+
url: BAM_TOKEN_URL,
|
|
762
|
+
headers: {
|
|
763
|
+
'Authorization': 'Bearer ' + await this.getClientApiKey() || this.halt('missing clientApiKey'),
|
|
764
|
+
'Origin': 'https://www.mlb.com'
|
|
765
|
+
},
|
|
766
|
+
form: {
|
|
767
|
+
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
|
|
768
|
+
'latitude': '0',
|
|
769
|
+
'longitude': '0',
|
|
770
|
+
'platform': 'browser',
|
|
771
|
+
'subject_token': await this.getDevicesAssertion() || this.halt('missing devicesAssertion'),
|
|
772
|
+
'subject_token_type': 'urn:bamtech:params:oauth:token-type:device'
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
var response = await this.httpPost(reqObj)
|
|
776
|
+
if ( this.isValidJson(response) ) {
|
|
777
|
+
this.debuglog('getDeviceAccessToken response : ' + response)
|
|
778
|
+
let obj = JSON.parse(response)
|
|
779
|
+
this.debuglog('getDeviceAccessToken : ' + obj.access_token)
|
|
780
|
+
return obj.access_token
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// API call
|
|
785
|
+
async getDevicesAssertion() {
|
|
786
|
+
this.debuglog('getDevicesAssertion')
|
|
787
|
+
let reqObj = {
|
|
788
|
+
url: 'https://us.edge.bamgrid.com/devices',
|
|
789
|
+
headers: {
|
|
790
|
+
'Authorization': 'Bearer ' + await this.getClientApiKey() || this.halt('missing clientApiKey'),
|
|
791
|
+
'Origin': 'https://www.mlb.com'
|
|
792
|
+
},
|
|
793
|
+
json: {
|
|
794
|
+
'applicationRuntime': 'firefox',
|
|
795
|
+
'attributes': {},
|
|
796
|
+
'deviceFamily': 'browser',
|
|
797
|
+
'deviceProfile': 'macosx'
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
var response = await this.httpPost(reqObj)
|
|
801
|
+
if ( response.assertion ) {
|
|
802
|
+
this.debuglog('getDevicesAssertion response : ' + response)
|
|
803
|
+
this.debuglog('getDevicesAssertion : ' + response.assertion)
|
|
804
|
+
return response.assertion
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async getOktaAccessToken() {
|
|
809
|
+
this.debuglog('getOktaAccessToken')
|
|
810
|
+
let oktaAccessToken = await this.retrieveOktaAccessToken()
|
|
811
|
+
if ( oktaAccessToken ) return oktaAccessToken
|
|
812
|
+
else {
|
|
813
|
+
oktaAccessToken = await this.retrieveOktaAccessToken()
|
|
814
|
+
if ( oktaAccessToken ) return oktaAccessToken
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// API call
|
|
819
|
+
async retrieveOktaAccessToken() {
|
|
820
|
+
this.debuglog('retrieveOktaAccessToken')
|
|
821
|
+
if ( !this.data.oktaAccessToken || !this.data.oktaAccessTokenExpiry || (Date.parse(this.data.oktaAccessTokenExpiry) < new Date()) ) {
|
|
822
|
+
this.debuglog('need to get new oktaAccessToken')
|
|
823
|
+
let state = this.getRandomString(64)
|
|
824
|
+
let nonce = this.getRandomString(64)
|
|
825
|
+
let reqObj = {
|
|
826
|
+
url: 'https://ids.mlb.com/oauth2/aus1m088yK07noBfh356/v1/authorize',
|
|
827
|
+
headers: {
|
|
828
|
+
'user-agent': USER_AGENT,
|
|
829
|
+
'accept-encoding': 'identity'
|
|
830
|
+
},
|
|
831
|
+
qs: {
|
|
832
|
+
'client_id': await this.getOktaClientId() || this.halt('missing oktaClientId'),
|
|
833
|
+
'redirect_uri': 'https://www.mlb.com/login',
|
|
834
|
+
'response_type': 'id_token token',
|
|
835
|
+
'response_mode': 'okta_post_message',
|
|
836
|
+
'state': state,
|
|
837
|
+
'nonce': nonce,
|
|
838
|
+
'prompt': 'none',
|
|
839
|
+
'sessionToken': await this.getAuthnSessionToken() || this.halt('missing authnSessionToken'),
|
|
840
|
+
'scope': 'openid email'
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
var response = await this.httpGet(reqObj)
|
|
844
|
+
var str = response.toString()
|
|
845
|
+
this.debuglog('retrieveOktaAccessToken response : ' + str)
|
|
846
|
+
if ( str.match ) {
|
|
847
|
+
var errorParsed = str.match("data.error = 'login_required'")
|
|
848
|
+
if ( errorParsed && errorParsed[1] ) {
|
|
849
|
+
// Need to log in again
|
|
850
|
+
this.log('Logging in...')
|
|
851
|
+
this.data.authnSessionToken = null
|
|
852
|
+
this.save_session_data()
|
|
853
|
+
return false
|
|
854
|
+
} else {
|
|
855
|
+
var parsed_token = str.match("data.access_token = '([^']+)'")
|
|
856
|
+
var parsed_expiry = str.match("data.expires_in = '([^']+)'")
|
|
857
|
+
if ( parsed_token && parsed_token[1] && parsed_expiry && parsed_expiry[1] ) {
|
|
858
|
+
let oktaAccessToken = parsed_token[1].split('\\x2D').join('-')
|
|
859
|
+
let oktaAccessTokenExpiry = parsed_expiry[1]
|
|
860
|
+
this.debuglog('retrieveOktaAccessToken : ' + oktaAccessToken)
|
|
861
|
+
this.debuglog('retrieveOktaAccessToken expires in : ' + oktaAccessTokenExpiry)
|
|
862
|
+
this.data.oktaAccessToken = oktaAccessToken
|
|
863
|
+
this.data.oktaAccessTokenExpiry = new Date(new Date().getTime() + oktaAccessTokenExpiry * 1000)
|
|
864
|
+
this.save_session_data()
|
|
865
|
+
return this.data.oktaAccessToken
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
} else {
|
|
870
|
+
return this.data.oktaAccessToken
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// API call
|
|
875
|
+
async getAuthnSessionToken() {
|
|
876
|
+
this.debuglog('getAuthnSessionToken')
|
|
877
|
+
if ( !this.data.authnSessionToken ) {
|
|
878
|
+
this.debuglog('need to get authnSessionToken')
|
|
879
|
+
let reqObj = {
|
|
880
|
+
url: 'https://ids.mlb.com/api/v1/authn',
|
|
881
|
+
headers: {
|
|
882
|
+
'user-agent': USER_AGENT,
|
|
883
|
+
'accept-encoding': 'identity',
|
|
884
|
+
'content-type': 'application/json'
|
|
885
|
+
},
|
|
886
|
+
json: {
|
|
887
|
+
'username': this.credentials.account_username || this.halt('missing account username'),
|
|
888
|
+
'password': this.credentials.account_password || this.halt('missing account password'),
|
|
889
|
+
'options': {
|
|
890
|
+
'multiOptionalFactorEnroll': false,
|
|
891
|
+
'warnBeforePasswordExpired': true
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
var response = await this.httpPost(reqObj)
|
|
896
|
+
if ( response.sessionToken ) {
|
|
897
|
+
this.debuglog('getAuthnSessionToken response : ' + JSON.stringify(response))
|
|
898
|
+
this.debuglog('getAuthnSessionToken : ' + response.sessionToken)
|
|
899
|
+
this.data.authnSessionToken = response.sessionToken
|
|
900
|
+
this.save_session_data()
|
|
901
|
+
return this.data.authnSessionToken
|
|
902
|
+
}
|
|
903
|
+
} else {
|
|
904
|
+
return this.data.authnSessionToken
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// get mediaId for a live channel request
|
|
909
|
+
async getMediaId(team, mediaType, mediaDate, gameNumber) {
|
|
910
|
+
try {
|
|
911
|
+
this.debuglog('getMediaId')
|
|
912
|
+
|
|
913
|
+
var mediaFeedType = 'mediaFeedType'
|
|
914
|
+
if ( mediaType == 'Video' ) {
|
|
915
|
+
mediaType = 'MLBTV'
|
|
916
|
+
} else if ( mediaType == 'Audio' ) {
|
|
917
|
+
mediaFeedType = 'type'
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
let gameDate = this.liveDate()
|
|
921
|
+
if ( mediaDate == 'yesterday' ) {
|
|
922
|
+
gameDate = this.yesterdayDate()
|
|
923
|
+
} else if ( (mediaDate) && (mediaDate != 'today') ) {
|
|
924
|
+
gameDate = mediaDate
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
let mediaId = false
|
|
928
|
+
let contentId = false
|
|
929
|
+
|
|
930
|
+
// First check if cached day data is available and non-expired
|
|
931
|
+
// if not, just get data for this team
|
|
932
|
+
let cache_data
|
|
933
|
+
let cache_name = gameDate
|
|
934
|
+
let cache_file = path.join(CACHE_DIRECTORY, gameDate+'.json')
|
|
935
|
+
let currentDate = new Date()
|
|
936
|
+
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)) ) {
|
|
937
|
+
cache_data = await this.getDayData(gameDate)
|
|
938
|
+
} else {
|
|
939
|
+
cache_data = await this.getDayData(gameDate, team)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if ( (cache_data.totalGamesInProgress > 0) || (mediaDate) ) {
|
|
943
|
+
let nationalCount = 0
|
|
944
|
+
for (var j = 0; j < cache_data.dates[0].games.length; j++) {
|
|
945
|
+
if ( mediaId ) break
|
|
946
|
+
if ( (typeof cache_data.dates[0].games[j] !== 'undefined') && cache_data.dates[0].games[j].content && cache_data.dates[0].games[j].content.media && cache_data.dates[0].games[j].content.media.epg ) {
|
|
947
|
+
for (var k = 0; k < cache_data.dates[0].games[j].content.media.epg.length; k++) {
|
|
948
|
+
if ( mediaId ) break
|
|
949
|
+
if ( cache_data.dates[0].games[j].content.media.epg[k].title == mediaType ) {
|
|
950
|
+
for (var x = 0; x < cache_data.dates[0].games[j].content.media.epg[k].items.length; x++) {
|
|
951
|
+
if ( (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ON') || ((mediaDate) && ((cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ARCHIVE') || (cache_data.dates[0].games[j].status.abstractGameState == 'Final'))) ) {
|
|
952
|
+
if ( ((typeof cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType) == 'undefined') || (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaFeedType.indexOf('IN_MARKET_') == -1) ) {
|
|
953
|
+
if ( (team.toUpperCase().indexOf('NATIONAL.') == 0) && (cache_data.dates[0].games[j].content.media.epg[k].items[x][mediaFeedType] == 'NATIONAL') ) {
|
|
954
|
+
nationalCount += 1
|
|
955
|
+
let nationalArray = team.split('.')
|
|
956
|
+
if ( (nationalArray.length == 2) && (nationalArray[1] == nationalCount) ) {
|
|
957
|
+
this.debuglog('matched national event')
|
|
958
|
+
mediaId = cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaId
|
|
959
|
+
contentId = cache_data.dates[0].games[j].content.media.epg[k].items[x].contentId
|
|
960
|
+
break
|
|
961
|
+
}
|
|
962
|
+
} else {
|
|
963
|
+
let teamType = cache_data.dates[0].games[j].content.media.epg[k].items[x][mediaFeedType].toLowerCase()
|
|
964
|
+
if ( (teamType != 'national') && (team.toUpperCase() == cache_data.dates[0].games[j].teams[teamType].team.abbreviation) ) {
|
|
965
|
+
if ( gameNumber && (gameNumber > 1) ) {
|
|
966
|
+
this.debuglog('matched team for game number 1')
|
|
967
|
+
gameNumber--
|
|
968
|
+
} else {
|
|
969
|
+
this.debuglog('matched team for event')
|
|
970
|
+
mediaId = cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaId
|
|
971
|
+
contentId = cache_data.dates[0].games[j].content.media.epg[k].items[x].contentId
|
|
972
|
+
break
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (mediaId) {
|
|
985
|
+
return { mediaId, contentId }
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
this.log('could not find mediaId')
|
|
989
|
+
} catch(e) {
|
|
990
|
+
this.log('getMediaId error : ' + e.message)
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// get highlights for a game
|
|
995
|
+
async getHighlightsData(gamePk, gameDate) {
|
|
996
|
+
try {
|
|
997
|
+
this.debuglog('getHighlightsData for ' + gamePk)
|
|
998
|
+
|
|
999
|
+
let cache_data
|
|
1000
|
+
let cache_name = 'h' + gamePk
|
|
1001
|
+
let cache_file = path.join(CACHE_DIRECTORY, cache_name+'.json')
|
|
1002
|
+
let currentDate = new Date()
|
|
1003
|
+
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)) ) {
|
|
1004
|
+
let reqObj = {
|
|
1005
|
+
url: 'https://statsapi.mlb.com/api/v1/game/' + gamePk + '/content',
|
|
1006
|
+
headers: {
|
|
1007
|
+
'User-agent': USER_AGENT,
|
|
1008
|
+
'Origin': 'https://www.mlb.com',
|
|
1009
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
1010
|
+
'Content-type': 'application/json'
|
|
1011
|
+
},
|
|
1012
|
+
gzip: true
|
|
1013
|
+
}
|
|
1014
|
+
var response = await this.httpGet(reqObj)
|
|
1015
|
+
if ( this.isValidJson(response) ) {
|
|
1016
|
+
//this.debuglog(response)
|
|
1017
|
+
cache_data = JSON.parse(response)
|
|
1018
|
+
this.save_json_cache_file(cache_name, cache_data)
|
|
1019
|
+
|
|
1020
|
+
// Default cache period is 1 hour from now
|
|
1021
|
+
let oneHourFromNow = new Date()
|
|
1022
|
+
oneHourFromNow.setHours(oneHourFromNow.getHours()+1)
|
|
1023
|
+
let cacheExpiry = oneHourFromNow
|
|
1024
|
+
|
|
1025
|
+
let today = this.liveDate()
|
|
1026
|
+
let yesterday = this.yesterdayDate()
|
|
1027
|
+
if ( gameDate == today ) {
|
|
1028
|
+
if ( (cache_data.media) && (cache_data.media.epg) ) {
|
|
1029
|
+
for (var i = 0; i < cache_data.media.epg.length; i++) {
|
|
1030
|
+
if ( cache_data.media.epg[i].items && cache_data.media.epg[i].items[0] && cache_data.media.epg[i].items[0].mediaState && (cache_data.media.epg[i].items[0].mediaState == 'MEDIA_ON') ) {
|
|
1031
|
+
this.debuglog('setting cache expiry to 5 minute due to in progress games')
|
|
1032
|
+
currentDate.setMinutes(currentDate.getMinutes()+5)
|
|
1033
|
+
cacheExpiry = currentDate
|
|
1034
|
+
break
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
} else if ( gameDate < today ) {
|
|
1039
|
+
this.debuglog('1+ days old, setting cache expiry to forever')
|
|
1040
|
+
cacheExpiry = new Date(8640000000000000)
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// finally save the setting
|
|
1044
|
+
this.setHighlightsCacheExpiry(cache_name, cacheExpiry)
|
|
1045
|
+
} else {
|
|
1046
|
+
this.log('error : invalid json from url ' + getObj.url)
|
|
1047
|
+
}
|
|
1048
|
+
} else {
|
|
1049
|
+
this.debuglog('using cached highlight data')
|
|
1050
|
+
cache_data = this.readFileToJson(cache_file)
|
|
1051
|
+
}
|
|
1052
|
+
if (cache_data && cache_data.highlights && cache_data.highlights.highlights && cache_data.highlights.highlights.items) {
|
|
1053
|
+
var array = cache_data.highlights.highlights.items
|
|
1054
|
+
return array.sort(this.GetSortOrder('date'))
|
|
1055
|
+
}
|
|
1056
|
+
} catch(e) {
|
|
1057
|
+
this.log('getHighlightsData error : ' + e.message)
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
GetSortOrder(prop) {
|
|
1062
|
+
return function(a, b) {
|
|
1063
|
+
if (a[prop] > b[prop]) {
|
|
1064
|
+
return 1
|
|
1065
|
+
} else if (a[prop] < b[prop]) {
|
|
1066
|
+
return -1
|
|
1067
|
+
}
|
|
1068
|
+
return 0
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// get data for a day, either from cache or an API call
|
|
1073
|
+
async getDayData(dateString, team = false) {
|
|
1074
|
+
try {
|
|
1075
|
+
let cache_data
|
|
1076
|
+
let cache_name = dateString
|
|
1077
|
+
let url = 'https://bdfed.stitch.mlbinfra.com/bdfed/transform-mlb-scoreboard?stitch_env=prod&sortTemplate=2&sportId=1&startDate=' + dateString + '&endDate=' + dateString + '&gameType=E&&gameType=S&&gameType=R&&gameType=F&&gameType=D&&gameType=L&&gameType=W&&gameType=A&language=en&leagueId=104&&leagueId=103&contextTeamId='
|
|
1078
|
+
if ( team ) {
|
|
1079
|
+
cache_name = team.toUpperCase() + dateString
|
|
1080
|
+
url = 'http://statsapi.mlb.com/api/v1/schedule?sportId=1&teamId=' + TEAM_IDS[team.toUpperCase()] + '&startDate=' + dateString + '&endDate=' + dateString + '&gameType=&gamePk=&hydrate=team,game(content(media(epg)))'
|
|
1081
|
+
}
|
|
1082
|
+
this.debuglog('getDayData for ' + cache_name)
|
|
1083
|
+
let cache_file = path.join(CACHE_DIRECTORY, cache_name+'.json')
|
|
1084
|
+
let currentDate = new Date()
|
|
1085
|
+
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)) ) {
|
|
1086
|
+
let reqObj = {
|
|
1087
|
+
url: url,
|
|
1088
|
+
headers: {
|
|
1089
|
+
'User-agent': USER_AGENT,
|
|
1090
|
+
'Origin': 'https://www.mlb.com',
|
|
1091
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
1092
|
+
'Content-type': 'application/json'
|
|
1093
|
+
},
|
|
1094
|
+
gzip: true
|
|
1095
|
+
}
|
|
1096
|
+
var response = await this.httpGet(reqObj)
|
|
1097
|
+
if ( this.isValidJson(response) ) {
|
|
1098
|
+
//this.debuglog(response)
|
|
1099
|
+
cache_data = JSON.parse(response)
|
|
1100
|
+
this.save_json_cache_file(cache_name, cache_data)
|
|
1101
|
+
|
|
1102
|
+
// Default cache period is 1 hour from now
|
|
1103
|
+
let oneHourFromNow = new Date()
|
|
1104
|
+
oneHourFromNow.setHours(oneHourFromNow.getHours()+1)
|
|
1105
|
+
let cacheExpiry = oneHourFromNow
|
|
1106
|
+
|
|
1107
|
+
let today = this.liveDate()
|
|
1108
|
+
let yesterday = this.yesterdayDate()
|
|
1109
|
+
if ( dateString == today ) {
|
|
1110
|
+
let finals = false
|
|
1111
|
+
for (var i = 0; i < cache_data.dates[0].games.length; i++) {
|
|
1112
|
+
if ( ((cache_data.dates[0].games[i].status.abstractGameState == 'Live') && (cache_data.dates[0].games[i].status.detailedState.indexOf('Suspended') != 0)) || ((cache_data.dates[0].games[i].status.startTimeTBD == true) && (cache_data.dates[0].games[i].status.abstractGameState != 'Final') && (i > 0) && (cache_data.dates[0].games[i-1].status.abstractGameState == 'Final')) ) {
|
|
1113
|
+
this.debuglog('setting cache expiry to 1 minute due to in progress games or upcoming TBD game')
|
|
1114
|
+
currentDate.setMinutes(currentDate.getMinutes()+1)
|
|
1115
|
+
cacheExpiry = currentDate
|
|
1116
|
+
break
|
|
1117
|
+
} else if ( cache_data.dates[0].games[i].status.abstractGameState == 'Final' ) {
|
|
1118
|
+
finals = true
|
|
1119
|
+
} else if ( (finals == false) && (cache_data.dates[0].games[i].status.startTimeTBD == false) ) {
|
|
1120
|
+
let nextGameDate = new Date(cache_data.dates[0].games[i].gameDate)
|
|
1121
|
+
nextGameDate.setHours(nextGameDate.getHours()-1)
|
|
1122
|
+
this.debuglog('setting cache expiry to 1 hour before next live game')
|
|
1123
|
+
cacheExpiry = nextGameDate
|
|
1124
|
+
break
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
} else if ( dateString > today ) {
|
|
1128
|
+
this.debuglog('1+ days in the future, setting cache expiry to tomorrow')
|
|
1129
|
+
let tomorrowDate = new Date(today)
|
|
1130
|
+
tomorrowDate.setDate(tomorrowDate.getDate()+1)
|
|
1131
|
+
let utcHours = 10
|
|
1132
|
+
tomorrowDate.setHours(tomorrowDate.getHours()+utcHours)
|
|
1133
|
+
cacheExpiry = tomorrowDate
|
|
1134
|
+
} else if ( dateString < yesterday ) {
|
|
1135
|
+
this.debuglog('2+ days old, setting cache expiry to forever')
|
|
1136
|
+
cacheExpiry = new Date(8640000000000000)
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// finally save the setting
|
|
1140
|
+
this.setDateCacheExpiry(cache_name, cacheExpiry)
|
|
1141
|
+
} else {
|
|
1142
|
+
this.log('error : invalid json from url ' + getObj.url)
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
this.debuglog('using cached date data')
|
|
1146
|
+
cache_data = this.readFileToJson(cache_file)
|
|
1147
|
+
}
|
|
1148
|
+
if (cache_data) {
|
|
1149
|
+
return cache_data
|
|
1150
|
+
}
|
|
1151
|
+
} catch(e) {
|
|
1152
|
+
this.log('getDayData error : ' + e.message)
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// get data for 3 weeks, either from cache or an API call
|
|
1157
|
+
async getWeeksData() {
|
|
1158
|
+
try {
|
|
1159
|
+
this.debuglog('getWeeksData')
|
|
1160
|
+
|
|
1161
|
+
// use 5 AM UTC time as the threshold to advance 1 day
|
|
1162
|
+
let utcHours = 5
|
|
1163
|
+
|
|
1164
|
+
let cache_data
|
|
1165
|
+
let cache_name = 'week'
|
|
1166
|
+
let cache_file = path.join(CACHE_DIRECTORY, cache_name + '.json')
|
|
1167
|
+
let currentDate = new Date()
|
|
1168
|
+
if ( !fs.existsSync(cache_file) || !this.cache || !this.cache.weekCacheExpiry || (currentDate > new Date(this.cache.weekCacheExpiry)) ) {
|
|
1169
|
+
let startDate = this.liveDate(utcHours)
|
|
1170
|
+
let endDate = new Date(startDate)
|
|
1171
|
+
endDate.setDate(endDate.getDate()+20)
|
|
1172
|
+
endDate = endDate.toISOString().substring(0,10)
|
|
1173
|
+
let reqObj = {
|
|
1174
|
+
url: 'https://bdfed.stitch.mlbinfra.com/bdfed/transform-mlb-scoreboard?stitch_env=prod&sortTemplate=2&sportId=1&startDate=' + startDate + '&endDate=' + endDate + '&gameType=E&&gameType=S&&gameType=R&&gameType=F&&gameType=D&&gameType=L&&gameType=W&&gameType=A&language=en&leagueId=104&&leagueId=103&contextTeamId=',
|
|
1175
|
+
headers: {
|
|
1176
|
+
'User-agent': USER_AGENT,
|
|
1177
|
+
'Origin': 'https://www.mlb.com',
|
|
1178
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
1179
|
+
'Content-type': 'application/json'
|
|
1180
|
+
},
|
|
1181
|
+
gzip: true
|
|
1182
|
+
}
|
|
1183
|
+
var response = await this.httpGet(reqObj)
|
|
1184
|
+
if ( this.isValidJson(response) ) {
|
|
1185
|
+
//this.debuglog(response)
|
|
1186
|
+
cache_data = JSON.parse(response)
|
|
1187
|
+
this.save_json_cache_file(cache_name, cache_data)
|
|
1188
|
+
this.debuglog('setting channels cache expiry to next day')
|
|
1189
|
+
let nextDate = new Date(startDate)
|
|
1190
|
+
nextDate.setDate(nextDate.getDate()+1)
|
|
1191
|
+
nextDate.setHours(nextDate.getHours()+utcHours)
|
|
1192
|
+
this.cache.weekCacheExpiry = nextDate
|
|
1193
|
+
this.save_cache_data()
|
|
1194
|
+
} else {
|
|
1195
|
+
this.log('error : invalid json from url ' + getObj.url)
|
|
1196
|
+
}
|
|
1197
|
+
} else {
|
|
1198
|
+
this.debuglog('using cached channel data')
|
|
1199
|
+
cache_data = this.readFileToJson(cache_file)
|
|
1200
|
+
}
|
|
1201
|
+
if (cache_data) {
|
|
1202
|
+
return cache_data
|
|
1203
|
+
}
|
|
1204
|
+
} catch(e) {
|
|
1205
|
+
this.log('getWeeksData error : ' + e.message)
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// get live channels in M3U format
|
|
1210
|
+
async getChannels(mediaType, includeTeams, excludeTeams, server, resolution, pipe, startingChannelNumber) {
|
|
1211
|
+
try {
|
|
1212
|
+
this.debuglog('getChannels')
|
|
1213
|
+
|
|
1214
|
+
var mediaFeedType = 'mediaFeedType'
|
|
1215
|
+
if ( mediaType == 'Video' ) {
|
|
1216
|
+
mediaType = 'MLBTV'
|
|
1217
|
+
} else if ( mediaType == 'Audio' ) {
|
|
1218
|
+
mediaFeedType = 'type'
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
let cache_data = await this.getWeeksData()
|
|
1222
|
+
if (cache_data) {
|
|
1223
|
+
var channels = {}
|
|
1224
|
+
var nationalChannels = {}
|
|
1225
|
+
let prevDateIndex = {MLBTV:-1,Audio:-1}
|
|
1226
|
+
for (var i = 0; i < cache_data.dates.length; i++) {
|
|
1227
|
+
let dateIndex = {MLBTV:i,Audio:i}
|
|
1228
|
+
let nationalCounter = {MLBTV:0,Audio:0}
|
|
1229
|
+
for (var j = 0; j < cache_data.dates[i].games.length; j++) {
|
|
1230
|
+
if ( cache_data.dates[i].games[j].content && cache_data.dates[i].games[j].content.media && cache_data.dates[i].games[j].content.media.epg ) {
|
|
1231
|
+
for (var k = 0; k < cache_data.dates[i].games[j].content.media.epg.length; k++) {
|
|
1232
|
+
let mediaTitle = cache_data.dates[i].games[j].content.media.epg[k].title
|
|
1233
|
+
if ( mediaType == mediaTitle ) {
|
|
1234
|
+
for (var x = 0; x < cache_data.dates[i].games[j].content.media.epg[k].items.length; x++) {
|
|
1235
|
+
if ( ((((typeof cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedType) == 'undefined') || (cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedType.indexOf('IN_MARKET_') == -1)) && (((typeof cache_data.dates[i].games[j].content.media.epg[k].items[x].language) == 'undefined') || (cache_data.dates[i].games[j].content.media.epg[k].items[x].language != 'es'))) ) {
|
|
1236
|
+
let teamType = cache_data.dates[i].games[j].content.media.epg[k].items[x][mediaFeedType]
|
|
1237
|
+
let team
|
|
1238
|
+
let opponent_team
|
|
1239
|
+
if ( teamType == 'NATIONAL' ) {
|
|
1240
|
+
team = cache_data.dates[i].games[j].teams['home'].team.abbreviation
|
|
1241
|
+
opponent_team = cache_data.dates[i].games[j].teams['away'].team.abbreviation
|
|
1242
|
+
|
|
1243
|
+
if ( dateIndex[mediaTitle] > prevDateIndex[mediaTitle] ) {
|
|
1244
|
+
prevDateIndex[mediaTitle] = dateIndex[mediaTitle]
|
|
1245
|
+
nationalCounter[mediaTitle] = 1
|
|
1246
|
+
} else {
|
|
1247
|
+
nationalCounter[mediaTitle] += 1
|
|
1248
|
+
}
|
|
1249
|
+
} else {
|
|
1250
|
+
teamType = teamType.toLowerCase()
|
|
1251
|
+
let opponent_teamType = 'away'
|
|
1252
|
+
if ( teamType == 'away' ) {
|
|
1253
|
+
opponent_teamType = 'home'
|
|
1254
|
+
}
|
|
1255
|
+
team = cache_data.dates[i].games[j].teams[teamType].team.abbreviation
|
|
1256
|
+
opponent_team = cache_data.dates[i].games[j].teams[opponent_teamType].team.abbreviation
|
|
1257
|
+
}
|
|
1258
|
+
if ( (excludeTeams.length > 0) && (excludeTeams.includes(team) || excludeTeams.includes(opponent_team) || excludeTeams.includes(teamType)) ) {
|
|
1259
|
+
continue
|
|
1260
|
+
} else if ( (includeTeams.length == 0) || includeTeams.includes(team) || includeTeams.includes(teamType) ) {
|
|
1261
|
+
if ( (teamType == 'NATIONAL') && ((includeTeams.length == 0) || ((includeTeams.length > 0) && includeTeams.includes(teamType))) ) {
|
|
1262
|
+
team = teamType + '.' + nationalCounter[mediaTitle]
|
|
1263
|
+
}
|
|
1264
|
+
let channelid = mediaType + '.' + team
|
|
1265
|
+
let channelMediaType = mediaType
|
|
1266
|
+
if ( mediaType == 'MLBTV' ) {
|
|
1267
|
+
channelMediaType = 'Video'
|
|
1268
|
+
}
|
|
1269
|
+
let stream = server + '/stream.m3u8?team=' + encodeURIComponent(team) + '&mediaType=' + channelMediaType
|
|
1270
|
+
if ( channelMediaType == 'Video' ) {
|
|
1271
|
+
stream += '&resolution=' + resolution
|
|
1272
|
+
}
|
|
1273
|
+
if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
|
|
1274
|
+
if ( pipe == 'true' ) {
|
|
1275
|
+
stream = 'pipe://ffmpeg -hide_banner -loglevel fatal -i "' + stream + '" -map 0:v -map 0:a -c copy -metadata service_provider="MLBTV" -metadata service_name="' + channelid + '" -f mpegts pipe:1'
|
|
1276
|
+
}
|
|
1277
|
+
let icon = server
|
|
1278
|
+
if ( (teamType == 'NATIONAL') && ((includeTeams.length == 0) || ((includeTeams.length > 0) && includeTeams.includes(teamType))) ) {
|
|
1279
|
+
icon += '/image.svg?teamId=MLB'
|
|
1280
|
+
if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect
|
|
1281
|
+
nationalChannels[channelid] = {}
|
|
1282
|
+
nationalChannels[channelid].channellogo = icon
|
|
1283
|
+
nationalChannels[channelid].stream = stream
|
|
1284
|
+
nationalChannels[channelid].mediatype = mediaType
|
|
1285
|
+
} else {
|
|
1286
|
+
icon += '/image.svg?teamId=' + cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedSubType
|
|
1287
|
+
if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect
|
|
1288
|
+
channels[channelid] = {}
|
|
1289
|
+
channels[channelid].channellogo = icon
|
|
1290
|
+
channels[channelid].stream = stream
|
|
1291
|
+
channels[channelid].mediatype = mediaType
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
channels = this.sortObj(channels)
|
|
1302
|
+
channels = Object.assign(channels, nationalChannels)
|
|
1303
|
+
|
|
1304
|
+
// Big Inning
|
|
1305
|
+
if ( mediaType == 'MLBTV' ) {
|
|
1306
|
+
if ( (excludeTeams.length > 0) && excludeTeams.includes('BIGINNING') ) {
|
|
1307
|
+
// do nothing
|
|
1308
|
+
} else if ( (includeTeams.length == 0) || includeTeams.includes('BIGINNING') ) {
|
|
1309
|
+
let extraChannels = {}
|
|
1310
|
+
let channelid = mediaType + '.BIGINNING'
|
|
1311
|
+
let icon = server + '/image.svg?teamId=MLB'
|
|
1312
|
+
if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect
|
|
1313
|
+
let stream = server + '/stream.m3u8?type=biginning&mediaType=Video&resolution=' + resolution
|
|
1314
|
+
if ( this.protection.content_protect ) stream += '&content_protect=' + this.protection.content_protect
|
|
1315
|
+
if ( pipe == 'true' ) {
|
|
1316
|
+
stream = 'pipe://ffmpeg -hide_banner -loglevel fatal -i "' + stream + '" -map 0:v -map 0:a -c copy -metadata service_provider="MLBTV" -metadata service_name="' + channelid + '" -f mpegts pipe:1'
|
|
1317
|
+
}
|
|
1318
|
+
extraChannels[channelid] = {}
|
|
1319
|
+
extraChannels[channelid].channellogo = icon
|
|
1320
|
+
extraChannels[channelid].stream = stream
|
|
1321
|
+
extraChannels[channelid].mediatype = mediaType
|
|
1322
|
+
channels = Object.assign(channels, extraChannels)
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Multiview
|
|
1327
|
+
if ( (mediaType == 'MLBTV') && (typeof this.data.multiviewStreamURLPath !== 'undefined') ) {
|
|
1328
|
+
if ( (excludeTeams.length > 0) && excludeTeams.includes('MULTIVIEW') ) {
|
|
1329
|
+
// do nothing
|
|
1330
|
+
} else if ( (includeTeams.length == 0) || includeTeams.includes('MULTIVIEW') ) {
|
|
1331
|
+
let extraChannels = {}
|
|
1332
|
+
let channelid = mediaType + '.MULTIVIEW'
|
|
1333
|
+
let icon = server + '/image.svg?teamId=MLB'
|
|
1334
|
+
if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect
|
|
1335
|
+
let stream = server.replace(':' + this.data.port, ':' + this.data.multiviewPort) + this.data.multiviewStreamURLPath
|
|
1336
|
+
if ( pipe == 'true' ) {
|
|
1337
|
+
stream = 'pipe://ffmpeg -hide_banner -loglevel fatal -i "' + stream + '" -map 0:v -map 0:a -c copy -metadata service_provider="MLBTV" -metadata service_name="' + channelid + '" -f mpegts pipe:1'
|
|
1338
|
+
}
|
|
1339
|
+
extraChannels[channelid] = {}
|
|
1340
|
+
extraChannels[channelid].channellogo = icon
|
|
1341
|
+
extraChannels[channelid].stream = stream
|
|
1342
|
+
extraChannels[channelid].mediatype = mediaType
|
|
1343
|
+
channels = Object.assign(channels, extraChannels)
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
let channelnumber = startingChannelNumber
|
|
1348
|
+
var body = '#EXTM3U' + "\n"
|
|
1349
|
+
//body += '#EXTINF:-1 CUID="MLBSERVER.SAMPLE.VIDEO" tvg-id="MLBSERVER.SAMPLE.VIDEO" tvg-name="MLBSERVER.SAMPLE.VIDEO",MLBSERVER SAMPLE VIDEO' + "\n"
|
|
1350
|
+
//body += '/stream.m3u8' + "\n"
|
|
1351
|
+
for (const [key, value] of Object.entries(channels)) {
|
|
1352
|
+
body += '#EXTINF:-1 CUID="' + key + '" channelID="' + key + '" tvg-num="1.' + channelnumber + '" tvg-chno="1.' + channelnumber + '" tvg-id="' + key + '" tvg-name="' + key + '" tvg-logo="' + value.channellogo + '" group-title="' + value.mediatype + '",' + key + "\n"
|
|
1353
|
+
body += value.stream + "\n"
|
|
1354
|
+
channelnumber++
|
|
1355
|
+
}
|
|
1356
|
+
return body
|
|
1357
|
+
}
|
|
1358
|
+
} catch(e) {
|
|
1359
|
+
this.log('getChannels error : ' + e.message)
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// get guide.xml file, in XMLTV format
|
|
1364
|
+
async getGuide(mediaType, includeTeams, excludeTeams, server) {
|
|
1365
|
+
try {
|
|
1366
|
+
this.debuglog('getGuide')
|
|
1367
|
+
|
|
1368
|
+
var mediaFeedType = 'mediaFeedType'
|
|
1369
|
+
if ( mediaType == 'Video' ) {
|
|
1370
|
+
mediaType = 'MLBTV'
|
|
1371
|
+
} else if ( mediaType == 'Audio' ) {
|
|
1372
|
+
mediaFeedType = 'type'
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
let cache_data = await this.getWeeksData()
|
|
1376
|
+
if (cache_data) {
|
|
1377
|
+
var channels = {}
|
|
1378
|
+
var programs = ""
|
|
1379
|
+
let prevDateIndex = {MLBTV:-1,Audio:-1}
|
|
1380
|
+
for (var i = 0; i < cache_data.dates.length; i++) {
|
|
1381
|
+
let dateIndex = {MLBTV:i,Audio:i}
|
|
1382
|
+
let nationalCounter = {MLBTV:0,Audio:0}
|
|
1383
|
+
for (var j = 0; j < cache_data.dates[i].games.length; j++) {
|
|
1384
|
+
if ( cache_data.dates[i].games[j].content && cache_data.dates[i].games[j].content.media && cache_data.dates[i].games[j].content.media.epg ) {
|
|
1385
|
+
for (var k = 0; k < cache_data.dates[i].games[j].content.media.epg.length; k++) {
|
|
1386
|
+
let mediaTitle = cache_data.dates[i].games[j].content.media.epg[k].title
|
|
1387
|
+
if ( mediaTitle == mediaType ) {
|
|
1388
|
+
for (var x = 0; x < cache_data.dates[i].games[j].content.media.epg[k].items.length; x++) {
|
|
1389
|
+
if ( ((((typeof cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedType) == 'undefined') || (cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedType.indexOf('IN_MARKET_') == -1)) && (((typeof cache_data.dates[i].games[j].content.media.epg[k].items[x].language) == 'undefined') || (cache_data.dates[i].games[j].content.media.epg[k].items[x].language != 'es'))) ) {
|
|
1390
|
+
let teamType = cache_data.dates[i].games[j].content.media.epg[k].items[x][mediaFeedType]
|
|
1391
|
+
let team
|
|
1392
|
+
let opponent_team
|
|
1393
|
+
if ( teamType == 'NATIONAL' ) {
|
|
1394
|
+
team = cache_data.dates[i].games[j].teams['home'].team.abbreviation
|
|
1395
|
+
opponent_team = cache_data.dates[i].games[j].teams['away'].team.abbreviation
|
|
1396
|
+
|
|
1397
|
+
if ( dateIndex[mediaTitle] > prevDateIndex[mediaTitle] ) {
|
|
1398
|
+
prevDateIndex[mediaTitle] = dateIndex[mediaTitle]
|
|
1399
|
+
nationalCounter[mediaTitle] = 1
|
|
1400
|
+
} else {
|
|
1401
|
+
nationalCounter[mediaTitle] += 1
|
|
1402
|
+
}
|
|
1403
|
+
} else {
|
|
1404
|
+
teamType = teamType.toLowerCase()
|
|
1405
|
+
let opponent_teamType = 'away'
|
|
1406
|
+
if ( teamType == 'away' ) {
|
|
1407
|
+
opponent_teamType = 'home'
|
|
1408
|
+
}
|
|
1409
|
+
team = cache_data.dates[i].games[j].teams[teamType].team.abbreviation
|
|
1410
|
+
opponent_team = cache_data.dates[i].games[j].teams[opponent_teamType].team.abbreviation
|
|
1411
|
+
}
|
|
1412
|
+
if ( (excludeTeams.length > 0) && (excludeTeams.includes(team) || excludeTeams.includes(opponent_team) || excludeTeams.includes(teamType)) ) {
|
|
1413
|
+
continue
|
|
1414
|
+
} else if ( (includeTeams.length == 0) || includeTeams.includes(team) || includeTeams.includes(teamType) ) {
|
|
1415
|
+
let icon = server
|
|
1416
|
+
if ( (teamType == 'NATIONAL') && ((includeTeams.length == 0) || ((includeTeams.length > 0) && includeTeams.includes(teamType))) ) {
|
|
1417
|
+
team = teamType + '.' + nationalCounter[mediaTitle]
|
|
1418
|
+
icon += '/image.svg?teamId=MLB'
|
|
1419
|
+
} else {
|
|
1420
|
+
icon += '/image.svg?teamId=' + cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedSubType
|
|
1421
|
+
}
|
|
1422
|
+
if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect
|
|
1423
|
+
let channelid = mediaType + '.' + team
|
|
1424
|
+
channels[channelid] = {}
|
|
1425
|
+
channels[channelid].name = channelid
|
|
1426
|
+
channels[channelid].icon = icon
|
|
1427
|
+
|
|
1428
|
+
let title = 'MLB Baseball: ' + cache_data.dates[i].games[j].teams['away'].team.teamName + ' at ' + cache_data.dates[i].games[j].teams['home'].team.teamName + ' (' + cache_data.dates[i].games[j].content.media.epg[k].items[x].callLetters
|
|
1429
|
+
if ( mediaType == 'Audio' ) {
|
|
1430
|
+
title += ' Radio'
|
|
1431
|
+
}
|
|
1432
|
+
title += ')'
|
|
1433
|
+
|
|
1434
|
+
let description = ''
|
|
1435
|
+
if ( cache_data.dates[i].games[j].doubleHeader != 'N' ) {
|
|
1436
|
+
description += 'Game ' + cache_data.dates[i].games[j].gameNumber + '. '
|
|
1437
|
+
}
|
|
1438
|
+
if ( (cache_data.dates[i].games[j].teams['away'].probablePitcher && cache_data.dates[i].games[j].teams['away'].probablePitcher.fullName) || (cache_data.dates[i].games[j].teams['home'].probablePitcher && cache_data.dates[i].games[j].teams['home'].probablePitcher.fullName) ) {
|
|
1439
|
+
if ( cache_data.dates[i].games[j].teams['away'].probablePitcher && cache_data.dates[i].games[j].teams['away'].probablePitcher.fullName ) {
|
|
1440
|
+
description += cache_data.dates[i].games[j].teams['away'].probablePitcher.fullName
|
|
1441
|
+
} else {
|
|
1442
|
+
description += 'TBD'
|
|
1443
|
+
}
|
|
1444
|
+
description += ' vs. '
|
|
1445
|
+
if ( cache_data.dates[i].games[j].teams['home'].probablePitcher && cache_data.dates[i].games[j].teams['home'].probablePitcher.fullName ) {
|
|
1446
|
+
description += cache_data.dates[i].games[j].teams['home'].probablePitcher.fullName
|
|
1447
|
+
} else {
|
|
1448
|
+
description += 'TBD'
|
|
1449
|
+
}
|
|
1450
|
+
description += '. '
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
let gameDate = new Date(cache_data.dates[i].games[j].gameDate)
|
|
1454
|
+
let gameHours = 3
|
|
1455
|
+
// Handle suspended, TBD, and doubleheaders
|
|
1456
|
+
if ( cache_data.dates[i].games[j].status.resumedFrom ) {
|
|
1457
|
+
gameHours = 1
|
|
1458
|
+
if ( cache_data.dates[i].games[j].description ) {
|
|
1459
|
+
description += cache_data.dates[i].games[j].description
|
|
1460
|
+
} else {
|
|
1461
|
+
description += 'Resumption of suspended game.'
|
|
1462
|
+
}
|
|
1463
|
+
gameDate = new Date(cache_data.dates[i].games[j].gameDate)
|
|
1464
|
+
gameDate.setHours(gameDate.getHours()+1)
|
|
1465
|
+
} else if ( (cache_data.dates[i].games[j].status.startTimeTBD == true) && (cache_data.dates[i].games[j].doubleHeader == 'Y') && (cache_data.dates[i].games[j].gameNumber == 2) ) {
|
|
1466
|
+
description += 'Start time TBD.'
|
|
1467
|
+
gameDate = new Date(cache_data.dates[i].games[j-1].gameDate)
|
|
1468
|
+
gameDate.setHours(gameDate.getHours()+4)
|
|
1469
|
+
} else if ( cache_data.dates[i].games[j].status.startTimeTBD == true ) {
|
|
1470
|
+
continue
|
|
1471
|
+
}
|
|
1472
|
+
let start = this.convertDateToXMLTV(gameDate)
|
|
1473
|
+
gameDate.setHours(gameDate.getHours()+gameHours)
|
|
1474
|
+
let stop = this.convertDateToXMLTV(gameDate)
|
|
1475
|
+
|
|
1476
|
+
programs += "\n" + ' <programme channel="' + channelid + '" start="' + start + '" stop="' + stop + '">' + "\n" +
|
|
1477
|
+
' <title lang="en">' + title + '</title>' + "\n" +
|
|
1478
|
+
' <desc lang="en">' + description.trim() + '</desc>' + "\n" +
|
|
1479
|
+
' <category lang="en">Sports</category>' + "\n" +
|
|
1480
|
+
' <icon src="' + icon + '"></icon>' + "\n" +
|
|
1481
|
+
' </programme>'
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// Big Inning
|
|
1492
|
+
if ( mediaType == 'MLBTV' ) {
|
|
1493
|
+
if ( (excludeTeams.length > 0) && excludeTeams.includes('BIGINNING') ) {
|
|
1494
|
+
// do nothing
|
|
1495
|
+
} else if ( (includeTeams.length == 0) || includeTeams.includes('BIGINNING') ) {
|
|
1496
|
+
let icon = server + '/image.svg?teamId=MLB'
|
|
1497
|
+
if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect
|
|
1498
|
+
let channelid = mediaType + '.BIGINNING'
|
|
1499
|
+
channels[channelid] = {}
|
|
1500
|
+
channels[channelid].name = channelid
|
|
1501
|
+
channels[channelid].icon = icon
|
|
1502
|
+
|
|
1503
|
+
let title = 'MLB Big Inning'
|
|
1504
|
+
let description = 'Live look-ins and big moments from around the league'
|
|
1505
|
+
|
|
1506
|
+
await this.getBigInningSchedule()
|
|
1507
|
+
for (var i = 0; i < cache_data.dates.length; i++) {
|
|
1508
|
+
let gameDate = cache_data.dates[i].date
|
|
1509
|
+
if ( this.cache.bigInningSchedule[gameDate] && this.cache.bigInningSchedule[gameDate].start ) {
|
|
1510
|
+
let start = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].start))
|
|
1511
|
+
let stop = this.convertDateToXMLTV(new Date(this.cache.bigInningSchedule[gameDate].end))
|
|
1512
|
+
|
|
1513
|
+
programs += "\n" + ' <programme channel="' + channelid + '" start="' + start + '" stop="' + stop + '">' + "\n" +
|
|
1514
|
+
' <title lang="en">' + title + '</title>' + "\n" +
|
|
1515
|
+
' <desc lang="en">' + description.trim() + '</desc>' + "\n" +
|
|
1516
|
+
' <category lang="en">Sports</category>' + "\n" +
|
|
1517
|
+
' <icon src="' + icon + '"></icon>' + "\n" +
|
|
1518
|
+
' </programme>'
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// Multiview
|
|
1525
|
+
if ( (mediaType == 'MLBTV') && (typeof this.data.multiviewStreamURL !== 'undefined') ) {
|
|
1526
|
+
if ( (excludeTeams.length > 0) && excludeTeams.includes('MULTIVIEW') ) {
|
|
1527
|
+
// do nothing
|
|
1528
|
+
} else if ( (includeTeams.length == 0) || includeTeams.includes('MULTIVIEW') ) {
|
|
1529
|
+
let icon = server + '/image.svg?teamId=MLB'
|
|
1530
|
+
if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect
|
|
1531
|
+
let channelid = mediaType + '.MULTIVIEW'
|
|
1532
|
+
channels[channelid] = {}
|
|
1533
|
+
channels[channelid].name = channelid
|
|
1534
|
+
channels[channelid].icon = icon
|
|
1535
|
+
|
|
1536
|
+
let title = 'MLB Multiview'
|
|
1537
|
+
let description = 'Watch up to 4 games at once. Requires starting the multiview stream in the web interface first, and stopping it when done.'
|
|
1538
|
+
|
|
1539
|
+
for (var i = 0; i < cache_data.dates.length; i++) {
|
|
1540
|
+
let gameDate = new Date(cache_data.dates[i].date + 'T00:00:00.000')
|
|
1541
|
+
let start = this.convertDateToXMLTV(gameDate)
|
|
1542
|
+
gameDate.setDate(gameDate.getDate()+1)
|
|
1543
|
+
let stop = this.convertDateToXMLTV(gameDate)
|
|
1544
|
+
|
|
1545
|
+
programs += "\n" + ' <programme channel="' + channelid + '" start="' + start + '" stop="' + stop + '">' + "\n" +
|
|
1546
|
+
' <title lang="en">' + title + '</title>' + "\n" +
|
|
1547
|
+
' <desc lang="en">' + description.trim() + '</desc>' + "\n" +
|
|
1548
|
+
' <category lang="en">Sports</category>' + "\n" +
|
|
1549
|
+
' <icon src="' + icon + '"></icon>' + "\n" +
|
|
1550
|
+
' </programme>'
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
var body = '<?xml version="1.0" encoding="UTF-8"?>' + "\n" +
|
|
1556
|
+
'<!DOCTYPE tv SYSTEM "xmltv.dd">' + "\n" +
|
|
1557
|
+
' <tv generator-info-name="mlbserver" source-info-name="mlbserver">'
|
|
1558
|
+
for (const [key, value] of Object.entries(channels)) {
|
|
1559
|
+
body += "\n" + ' <channel id="' + key + '">' + "\n" +
|
|
1560
|
+
' <display-name>' + value.name + '</display-name>' + "\n" +
|
|
1561
|
+
' <icon src="' + value.icon + '"></icon>' + "\n" +
|
|
1562
|
+
' </channel>'
|
|
1563
|
+
}
|
|
1564
|
+
body += programs + "\n" + ' </tv>'
|
|
1565
|
+
|
|
1566
|
+
return body
|
|
1567
|
+
}
|
|
1568
|
+
} catch(e) {
|
|
1569
|
+
this.log('getGuide error : ' + e.message)
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// Get image from cache or request
|
|
1574
|
+
async getImage(teamId) {
|
|
1575
|
+
this.debuglog('getImage ' + teamId)
|
|
1576
|
+
let imagePath = path.join(CACHE_DIRECTORY, teamId + '.svg')
|
|
1577
|
+
if ( fs.existsSync(imagePath) ) {
|
|
1578
|
+
this.debuglog('using cached image for ' + teamId)
|
|
1579
|
+
return fs.readFileSync(imagePath)
|
|
1580
|
+
} else {
|
|
1581
|
+
this.debuglog('requesting new image for ' + teamId)
|
|
1582
|
+
let imageURL = 'https://www.mlbstatic.com/team-logos/' + teamId + '.svg'
|
|
1583
|
+
if ( teamId == 'MLB' ) {
|
|
1584
|
+
imageURL = 'https://www.mlbstatic.com/team-logos/league-on-dark/1.svg'
|
|
1585
|
+
}
|
|
1586
|
+
let reqObj = {
|
|
1587
|
+
url: imageURL,
|
|
1588
|
+
headers: {
|
|
1589
|
+
'User-agent': USER_AGENT,
|
|
1590
|
+
'origin': 'https://www.mlb.com'
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
var response = await this.httpGet(reqObj)
|
|
1594
|
+
if ( response ) {
|
|
1595
|
+
this.debuglog('getImage response : ' + response)
|
|
1596
|
+
fs.writeFileSync(imagePath, response)
|
|
1597
|
+
} else {
|
|
1598
|
+
this.debuglog('failed to get image for ' + teamId)
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Get airings data for a game
|
|
1604
|
+
async getAiringsData(contentId, gamePk = false) {
|
|
1605
|
+
try {
|
|
1606
|
+
this.debuglog('getAiringsData')
|
|
1607
|
+
|
|
1608
|
+
let cache_data
|
|
1609
|
+
let cache_name = contentId
|
|
1610
|
+
let cache_file = path.join(CACHE_DIRECTORY, contentId+'.json')
|
|
1611
|
+
let url = 'https://search-api-mlbtv.mlb.com/svc/search/v2/graphql/persisted/query/core/Airings'
|
|
1612
|
+
let qs = { variables: '%7B%22contentId%22%3A%22' + contentId + '%22%7D' }
|
|
1613
|
+
if ( gamePk ) {
|
|
1614
|
+
url = 'https://search-api-mlbtv.mlb.com/svc/search/v2/graphql/persisted/query/core/Airings?variables={%22partnerProgramIds%22%3A[%22' + gamePk + '%22]}'
|
|
1615
|
+
qs = {}
|
|
1616
|
+
}
|
|
1617
|
+
let currentDate = new Date()
|
|
1618
|
+
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)) ) {
|
|
1619
|
+
let reqObj = {
|
|
1620
|
+
url: url,
|
|
1621
|
+
qs: qs,
|
|
1622
|
+
headers: {
|
|
1623
|
+
'Accept': 'application/json',
|
|
1624
|
+
'X-BAMSDK-Version': BAM_SDK_VERSION,
|
|
1625
|
+
'X-BAMSDK-Platform': PLATFORM,
|
|
1626
|
+
'User-Agent': USER_AGENT,
|
|
1627
|
+
'Origin': 'https://www.mlb.com',
|
|
1628
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
1629
|
+
'Content-type': 'application/json'
|
|
1630
|
+
},
|
|
1631
|
+
gzip: true
|
|
1632
|
+
}
|
|
1633
|
+
var response = await this.httpGet(reqObj)
|
|
1634
|
+
if ( this.isValidJson(response) ) {
|
|
1635
|
+
this.debuglog(response)
|
|
1636
|
+
cache_data = JSON.parse(response)
|
|
1637
|
+
if ( gamePk ) {
|
|
1638
|
+
this.debuglog('searching for alternate airing with break data')
|
|
1639
|
+
let most_milestones = 0
|
|
1640
|
+
let most_milestones_index = -1
|
|
1641
|
+
let offset_index = 1
|
|
1642
|
+
let previous_offset
|
|
1643
|
+
let previous_startDatetime
|
|
1644
|
+
let new_startDatetime
|
|
1645
|
+
let new_offset
|
|
1646
|
+
for ( var i=0; i<cache_data.data.Airings.length; i++ ) {
|
|
1647
|
+
if ( cache_data.data.Airings[i].milestones ) {
|
|
1648
|
+
if ( cache_data.data.Airings[i].contentId && (cache_data.data.Airings[i].contentId == contentId) ) {
|
|
1649
|
+
if ( cache_data.data.Airings[i].milestones[0].milestoneTime[0].type == 'offset' ) offset_index = 0
|
|
1650
|
+
previous_offset = cache_data.data.Airings[i].milestones[0].milestoneTime[offset_index].start
|
|
1651
|
+
previous_startDatetime = new Date(cache_data.data.Airings[i].milestones[0].milestoneTime[(offset_index == 0 ? 1 : 0)].startDatetime)
|
|
1652
|
+
continue
|
|
1653
|
+
}
|
|
1654
|
+
if ( cache_data.data.Airings[i].milestones.length > most_milestones ) {
|
|
1655
|
+
most_milestones = cache_data.data.Airings[i].milestones.length
|
|
1656
|
+
most_milestones_index = i
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
if ( most_milestones_index && previous_startDatetime ) {
|
|
1661
|
+
this.debuglog('found alternate airing with break data')
|
|
1662
|
+
let temp_airing = cache_data.data.Airings[most_milestones_index]
|
|
1663
|
+
|
|
1664
|
+
offset_index = 1
|
|
1665
|
+
if ( temp_airing.milestones[0].milestoneTime[0].type == 'offset' ) offset_index = 0
|
|
1666
|
+
new_offset = temp_airing.milestones[0].milestoneTime[offset_index].start
|
|
1667
|
+
new_startDatetime = new Date(temp_airing.milestones[0].milestoneTime[(offset_index == 0 ? 1 : 0)].startDatetime)
|
|
1668
|
+
|
|
1669
|
+
let offset_adjust = (new_startDatetime / 1000) - (previous_startDatetime/1000) - previous_offset - new_offset
|
|
1670
|
+
this.debuglog('adjusting breaks by ' + offset_adjust)
|
|
1671
|
+
|
|
1672
|
+
for ( var j=0; j<temp_airing.milestones.length; j++ ) {
|
|
1673
|
+
offset_index = 1
|
|
1674
|
+
if ( temp_airing.milestones[j].milestoneTime[0].type == 'offset' ) offset_index = 0
|
|
1675
|
+
temp_airing.milestones[j].milestoneTime[offset_index].start += offset_adjust
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
cache_data.data.Airings = [{}]
|
|
1679
|
+
cache_data.data.Airings[0] = temp_airing
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
this.save_json_cache_file(contentId, cache_data)
|
|
1683
|
+
|
|
1684
|
+
// Default cache period is 1 hour from now
|
|
1685
|
+
let oneHourFromNow = new Date()
|
|
1686
|
+
oneHourFromNow.setHours(oneHourFromNow.getHours()+1)
|
|
1687
|
+
let cacheExpiry = oneHourFromNow
|
|
1688
|
+
|
|
1689
|
+
let today = this.liveDate()
|
|
1690
|
+
let game_date = new Date(cache_data.data.Airings[0].startDate)
|
|
1691
|
+
let compare_date = game_date.getFullYear() + '-' + (game_date.getMonth()+1).toString().padStart(2, '0') + '-' + game_date.getDate().toString().padStart(2, '0')
|
|
1692
|
+
|
|
1693
|
+
if ( compare_date == today ) {
|
|
1694
|
+
if ( (cache_data.data.Airings[0].mediaConfig.productType == 'LIVE') || ((typeof cache_data.data.Airings[0].milestones !== 'undefined') && (typeof cache_data.data.Airings[0].milestones[0] !== 'undefined') && (typeof cache_data.data.Airings[0].milestones[0].milestoneTime !== 'undefined') && (typeof cache_data.data.Airings[0].milestones[0].milestoneTime[0].start !== 'undefined') && ((cache_data.data.Airings[0].milestones[0].milestoneTime[0].start > 20*60) || (cache_data.data.Airings[0].milestones[0].milestoneTime[1].start > 20*60))) ) {
|
|
1695
|
+
this.debuglog('setting cache expiry to 5 minutes for live or untrimmed games today')
|
|
1696
|
+
currentDate.setMinutes(currentDate.getMinutes()+5)
|
|
1697
|
+
cacheExpiry = currentDate
|
|
1698
|
+
}
|
|
1699
|
+
} else if ( compare_date < today ) {
|
|
1700
|
+
this.debuglog('setting cache expiry to forever for past games')
|
|
1701
|
+
cacheExpiry = new Date(8640000000000000)
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// finally save the setting
|
|
1705
|
+
this.setAiringsCacheExpiry(cache_name, cacheExpiry)
|
|
1706
|
+
} else {
|
|
1707
|
+
this.log('error : invalid json from url ' + getObj.url)
|
|
1708
|
+
}
|
|
1709
|
+
} else {
|
|
1710
|
+
this.debuglog('using cached airings data')
|
|
1711
|
+
cache_data = this.readFileToJson(cache_file)
|
|
1712
|
+
}
|
|
1713
|
+
if (cache_data) {
|
|
1714
|
+
return cache_data
|
|
1715
|
+
}
|
|
1716
|
+
} catch(e) {
|
|
1717
|
+
this.log('getAiringsData error : ' + e.message)
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Get gameday data for a game (play and pitch data)
|
|
1722
|
+
async getGamedayData(contentId) {
|
|
1723
|
+
try {
|
|
1724
|
+
this.debuglog('getGamedayData')
|
|
1725
|
+
|
|
1726
|
+
let gamePk = await this.getGamePkFromContentId(contentId)
|
|
1727
|
+
|
|
1728
|
+
let cache_data
|
|
1729
|
+
let cache_name = 'g' + gamePk
|
|
1730
|
+
let cache_file = path.join(CACHE_DIRECTORY, cache_name+'.json')
|
|
1731
|
+
let currentDate = new Date()
|
|
1732
|
+
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)) ) {
|
|
1733
|
+
let reqObj = {
|
|
1734
|
+
url: 'http://statsapi.mlb.com/api/v1.1/game/' + gamePk + '/feed/live',
|
|
1735
|
+
headers: {
|
|
1736
|
+
'User-agent': USER_AGENT,
|
|
1737
|
+
'Origin': 'https://www.mlb.com',
|
|
1738
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
1739
|
+
'Content-type': 'application/json'
|
|
1740
|
+
},
|
|
1741
|
+
gzip: true
|
|
1742
|
+
}
|
|
1743
|
+
var response = await this.httpGet(reqObj)
|
|
1744
|
+
if ( this.isValidJson(response) ) {
|
|
1745
|
+
this.debuglog(response)
|
|
1746
|
+
cache_data = JSON.parse(response)
|
|
1747
|
+
this.save_json_cache_file(cache_name, cache_data)
|
|
1748
|
+
|
|
1749
|
+
// Default cache period is 1 hour from now
|
|
1750
|
+
let oneHourFromNow = new Date()
|
|
1751
|
+
oneHourFromNow.setHours(oneHourFromNow.getHours()+1)
|
|
1752
|
+
let cacheExpiry = oneHourFromNow
|
|
1753
|
+
|
|
1754
|
+
if ( (cache_data.gameData.status.abstractGameState == 'Live') && (cache_data.gameData.status.detailedState.indexOf('Suspended') != 0) ) {
|
|
1755
|
+
this.debuglog('setting cache expiry to 5 minutes for live game')
|
|
1756
|
+
currentDate.setMinutes(currentDate.getMinutes()+5)
|
|
1757
|
+
cacheExpiry = currentDate
|
|
1758
|
+
} else {
|
|
1759
|
+
let today = this.liveDate()
|
|
1760
|
+
|
|
1761
|
+
if ( cache_data.gameData.datetime.officialDate < today ) {
|
|
1762
|
+
this.debuglog('setting cache expiry to forever for past games')
|
|
1763
|
+
cacheExpiry = new Date(8640000000000000)
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// finally save the setting
|
|
1768
|
+
this.setGamedayCacheExpiry(cache_name, cacheExpiry)
|
|
1769
|
+
} else {
|
|
1770
|
+
this.log('error : invalid response from url ' + getObj.url)
|
|
1771
|
+
}
|
|
1772
|
+
} else {
|
|
1773
|
+
this.debuglog('using cached gameday data')
|
|
1774
|
+
cache_data = this.readFileToJson(cache_file)
|
|
1775
|
+
}
|
|
1776
|
+
if (cache_data) {
|
|
1777
|
+
return cache_data
|
|
1778
|
+
}
|
|
1779
|
+
} catch(e) {
|
|
1780
|
+
this.log('getGamedayData error : ' + e.message)
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Get broadcast start timestamp
|
|
1785
|
+
async getBroadcastStart(contentId) {
|
|
1786
|
+
try {
|
|
1787
|
+
this.debuglog('getBroadcastStart')
|
|
1788
|
+
|
|
1789
|
+
if ( !this.temp_cache[contentId] ) {
|
|
1790
|
+
this.temp_cache[contentId] = {}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
let cache_data = await this.getAiringsData(contentId)
|
|
1794
|
+
// If VOD and we have fewer than 2 milestones, use the gamePk to look for more milestones in a different airing
|
|
1795
|
+
if ( cache_data.data.Airings[0].mediaConfig.productType && (cache_data.data.Airings[0].mediaConfig.productType == 'VOD') && cache_data.data.Airings[0].milestones && (cache_data.data.Airings[0].milestones.length < 2) ) {
|
|
1796
|
+
this.log('too few milestones, looking for more')
|
|
1797
|
+
this.cache.airings[contentId] = {}
|
|
1798
|
+
cache_data = await this.getAiringsData(contentId, cache_data.data.Airings[0].partnerProgramId)
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
let broadcast_start_offset
|
|
1802
|
+
let broadcast_start_timestamp
|
|
1803
|
+
|
|
1804
|
+
if ( cache_data.data.Airings[0].milestones ) {
|
|
1805
|
+
for (var j = 0; j < cache_data.data.Airings[0].milestones.length; j++) {
|
|
1806
|
+
if ( cache_data.data.Airings[0].milestones[j].milestoneType == 'BROADCAST_START' ) {
|
|
1807
|
+
let offset_index = 1
|
|
1808
|
+
let offset
|
|
1809
|
+
if ( cache_data.data.Airings[0].milestones[j].milestoneTime[0].type == 'offset' ) {
|
|
1810
|
+
offset_index = 0
|
|
1811
|
+
}
|
|
1812
|
+
broadcast_start_offset = cache_data.data.Airings[0].milestones[j].milestoneTime[offset_index].start
|
|
1813
|
+
|
|
1814
|
+
// Broadcast start
|
|
1815
|
+
broadcast_start_timestamp = new Date(cache_data.data.Airings[0].milestones[j].milestoneTime[(offset_index == 0 ? 1 : 0)].startDatetime)
|
|
1816
|
+
this.debuglog('broadcast start time detected as ' + broadcast_start_timestamp)
|
|
1817
|
+
this.debuglog('offset detected as ' + broadcast_start_offset)
|
|
1818
|
+
broadcast_start_timestamp.setSeconds(broadcast_start_timestamp.getSeconds()-broadcast_start_offset)
|
|
1819
|
+
this.debuglog('new start time is ' + broadcast_start_timestamp)
|
|
1820
|
+
break
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
if ( broadcast_start_offset && broadcast_start_timestamp ) {
|
|
1826
|
+
return { broadcast_start_offset, broadcast_start_timestamp }
|
|
1827
|
+
}
|
|
1828
|
+
} catch(e) {
|
|
1829
|
+
this.log('getBroadcastStart error : ' + e.message)
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// Get event offsets into temporary cache
|
|
1834
|
+
async getEventOffsets(contentId, skip_types, skip_adjust = 0) {
|
|
1835
|
+
try {
|
|
1836
|
+
this.debuglog('getEventOffsets')
|
|
1837
|
+
|
|
1838
|
+
if ( skip_adjust != 0 ) session.log('manual adjustment of ' + skip_adjust + ' seconds being applied')
|
|
1839
|
+
|
|
1840
|
+
// Get the broadcast start time first -- event times will be relative to this
|
|
1841
|
+
let broadcast_start = await this.getBroadcastStart(contentId)
|
|
1842
|
+
let broadcast_start_offset = broadcast_start.broadcast_start_offset
|
|
1843
|
+
let broadcast_start_timestamp = broadcast_start.broadcast_start_timestamp
|
|
1844
|
+
this.debuglog('broadcast start detected as ' + broadcast_start_timestamp + ', offset ' + broadcast_start_offset)
|
|
1845
|
+
|
|
1846
|
+
let cache_data = await this.getGamedayData(contentId)
|
|
1847
|
+
|
|
1848
|
+
// There are the events to ignore, if we're skipping breaks
|
|
1849
|
+
let break_types = ['Game Advisory', 'Pitching Substitution', 'Offensive Substitution', 'Defensive Sub', 'Defensive Switch', 'Runner Placed On Base']
|
|
1850
|
+
|
|
1851
|
+
// There are the events to keep, in addition to the last event of each at-bat, if we're skipping pitches
|
|
1852
|
+
let action_types = ['Wild Pitch', 'Passed Ball', 'Stolen Base', 'Caught Stealing', 'Pickoff', 'Out', 'Balk', 'Defensive Indiff']
|
|
1853
|
+
|
|
1854
|
+
let inning_offsets = [{start:broadcast_start_offset}]
|
|
1855
|
+
let event_offsets = [{start:0}]
|
|
1856
|
+
let last_event = 0
|
|
1857
|
+
let default_event_duration = 15
|
|
1858
|
+
|
|
1859
|
+
// Pad times by these amounts
|
|
1860
|
+
let pad_start = 0
|
|
1861
|
+
let pad_end = 15
|
|
1862
|
+
let pad_adjust = 20
|
|
1863
|
+
|
|
1864
|
+
// Inning counters
|
|
1865
|
+
let last_inning = 0
|
|
1866
|
+
let last_inning_half = ''
|
|
1867
|
+
|
|
1868
|
+
// Loop through all plays
|
|
1869
|
+
for (var i=0; i < cache_data.liveData.plays.allPlays.length; i++) {
|
|
1870
|
+
|
|
1871
|
+
// If requested, calculate inning offsets
|
|
1872
|
+
if ( skip_types.includes('innings') ) {
|
|
1873
|
+
// Look for a change from our inning counters
|
|
1874
|
+
if ( cache_data.liveData.plays.allPlays[i].about && cache_data.liveData.plays.allPlays[i].about.inning && ((cache_data.liveData.plays.allPlays[i].about.inning != last_inning) || (cache_data.liveData.plays.allPlays[i].about.halfInning != last_inning_half)) ) {
|
|
1875
|
+
let inning_index = cache_data.liveData.plays.allPlays[i].about.inning * 2
|
|
1876
|
+
// top
|
|
1877
|
+
if ( cache_data.liveData.plays.allPlays[i].about.halfInning == 'top' ) {
|
|
1878
|
+
inning_index = inning_index - 1
|
|
1879
|
+
}
|
|
1880
|
+
if ( typeof inning_offsets[inning_index] === 'undefined' ) inning_offsets.push({})
|
|
1881
|
+
for (var j=0; j < cache_data.liveData.plays.allPlays[i].playEvents.length; j++) {
|
|
1882
|
+
if ( cache_data.liveData.plays.allPlays[i].playEvents[j].details && cache_data.liveData.plays.allPlays[i].playEvents[j].details.event && (break_types.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.event.includes(v))) ) {
|
|
1883
|
+
// ignore break events
|
|
1884
|
+
} else {
|
|
1885
|
+
inning_offsets[inning_index].start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[j].startTime) - broadcast_start_timestamp) / 1000) - pad_start + skip_adjust
|
|
1886
|
+
break
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
// Update inning counters
|
|
1890
|
+
last_inning = cache_data.liveData.plays.allPlays[i].about.inning
|
|
1891
|
+
last_inning_half = cache_data.liveData.plays.allPlays[i].about.halfInning
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Get event offsets, if necessary
|
|
1896
|
+
if ( skip_types.includes('breaks') || skip_types.includes('pitches') ) {
|
|
1897
|
+
|
|
1898
|
+
// Loop through play events, looking for actions
|
|
1899
|
+
let actions = []
|
|
1900
|
+
for (var j=0; j < cache_data.liveData.plays.allPlays[i].playEvents.length; j++) {
|
|
1901
|
+
// If skipping breaks, everything is an action except break types
|
|
1902
|
+
// otherwise, only action types are included (skipping pitches)
|
|
1903
|
+
if ( skip_types.includes('breaks') ) {
|
|
1904
|
+
if ( cache_data.liveData.plays.allPlays[i].playEvents[j].details && cache_data.liveData.plays.allPlays[i].playEvents[j].details.event && (break_types.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.event.includes(v))) ) {
|
|
1905
|
+
// ignore break events
|
|
1906
|
+
} else {
|
|
1907
|
+
actions.push(j)
|
|
1908
|
+
}
|
|
1909
|
+
} else if ( cache_data.liveData.plays.allPlays[i].playEvents[j].details && cache_data.liveData.plays.allPlays[i].playEvents[j].details.event && (action_types.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.event.includes(v))) ) {
|
|
1910
|
+
actions.push(j)
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// Process breaks
|
|
1915
|
+
if ( skip_types.includes('breaks') ) {
|
|
1916
|
+
let this_event = {}
|
|
1917
|
+
let event_in_atbat = false
|
|
1918
|
+
for (var x=0; x < actions.length; x++) {
|
|
1919
|
+
let this_pad_start = 0
|
|
1920
|
+
let this_pad_end = 0
|
|
1921
|
+
// Once we define each event's start time, we won't change it
|
|
1922
|
+
if ( typeof this_event.start === 'undefined' ) {
|
|
1923
|
+
this_event.start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].startTime) - broadcast_start_timestamp) / 1000) - pad_start + skip_adjust
|
|
1924
|
+
// For events within at-bats, adjust the padding
|
|
1925
|
+
if ( event_in_atbat ) {
|
|
1926
|
+
this_event.start -= pad_adjust
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
// Update the end time, if available
|
|
1930
|
+
if ( cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].endTime ) {
|
|
1931
|
+
this_event.end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].endTime) - broadcast_start_timestamp) / 1000) + pad_end + skip_adjust
|
|
1932
|
+
// Otherwise use the start time to estimate the end time
|
|
1933
|
+
} else {
|
|
1934
|
+
this_event.end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].startTime) - broadcast_start_timestamp) / 1000) + this_pad_end + skip_adjust + default_event_duration
|
|
1935
|
+
}
|
|
1936
|
+
// Check if we have skipped a play event (indicating a break inside an at-bat), in which case push this event and start another one
|
|
1937
|
+
if ( (x > 0) && (actions[x] > (actions[x-1]+1)) && (typeof this_event.end !== 'undefined') ) {
|
|
1938
|
+
// For events within at-bats, adjust the padding
|
|
1939
|
+
event_in_atbat = true
|
|
1940
|
+
this_event.end += pad_adjust
|
|
1941
|
+
event_offsets.push(this_event)
|
|
1942
|
+
this_event = {}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
// Once we've finished our loop through a play's events, push the event as long as we got an end time
|
|
1946
|
+
if ( typeof this_event.end !== 'undefined' ) {
|
|
1947
|
+
event_offsets.push(this_event)
|
|
1948
|
+
}
|
|
1949
|
+
} else if ( skip_types.includes('pitches') ) {
|
|
1950
|
+
// If we're skipping pitches, but we didn't detect any action events, use the last play event
|
|
1951
|
+
if ( (cache_data.liveData.plays.allPlays[i].playEvents.length > 0) && ((actions.length == 0) || (actions[(actions.length-1)] < (cache_data.liveData.plays.allPlays[i].playEvents.length-1))) ) {
|
|
1952
|
+
actions.push(cache_data.liveData.plays.allPlays[i].playEvents.length-1)
|
|
1953
|
+
}
|
|
1954
|
+
// Loop through the actions
|
|
1955
|
+
for (var x=0; x < actions.length; x++) {
|
|
1956
|
+
let this_event = {}
|
|
1957
|
+
let this_pad_start = pad_start
|
|
1958
|
+
let this_pad_end = pad_end
|
|
1959
|
+
// For events within at-bats, adjust the padding
|
|
1960
|
+
if ( x < (actions.length-1) ) {
|
|
1961
|
+
this_pad_start += pad_adjust
|
|
1962
|
+
this_pad_end -= pad_adjust
|
|
1963
|
+
}
|
|
1964
|
+
this_event.start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].startTime) - broadcast_start_timestamp) / 1000) - this_pad_start + skip_adjust
|
|
1965
|
+
// If play event end time is available, set it and push this event
|
|
1966
|
+
if ( cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].endTime ) {
|
|
1967
|
+
this_event.end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].endTime) - broadcast_start_timestamp) / 1000) + this_pad_end + skip_adjust
|
|
1968
|
+
// Otherwise use the start time to estimate the end time
|
|
1969
|
+
} else {
|
|
1970
|
+
this_event.end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].startTime) - broadcast_start_timestamp) / 1000) + this_pad_end + skip_adjust + default_event_duration
|
|
1971
|
+
}
|
|
1972
|
+
event_offsets.push(this_event)
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
if ( skip_types.includes('innings') ) {
|
|
1979
|
+
this.debuglog('inning offsets: ' + JSON.stringify(inning_offsets))
|
|
1980
|
+
}
|
|
1981
|
+
this.temp_cache[contentId].inning_offsets = inning_offsets
|
|
1982
|
+
|
|
1983
|
+
if ( skip_types.includes('breaks') || skip_types.includes('pitches') ) {
|
|
1984
|
+
this.debuglog('event offsets: ' + JSON.stringify(event_offsets))
|
|
1985
|
+
}
|
|
1986
|
+
this.temp_cache[contentId].event_offsets = event_offsets
|
|
1987
|
+
|
|
1988
|
+
return true
|
|
1989
|
+
} catch(e) {
|
|
1990
|
+
this.log('getEventOffsets error : ' + e.message)
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// Get Big Inning schedule, if available
|
|
1995
|
+
async getBigInningSchedule(dateString = false) {
|
|
1996
|
+
try {
|
|
1997
|
+
this.debuglog('getBigInningSchedule')
|
|
1998
|
+
|
|
1999
|
+
let currentDate = new Date()
|
|
2000
|
+
if ( !this.cache || !this.cache.bigInningScheduleCacheExpiry || (currentDate > new Date(this.cache.bigInningScheduleCacheExpiry)) ) {
|
|
2001
|
+
if ( !this.cache.bigInningSchedule ) this.cache.bigInningSchedule = {}
|
|
2002
|
+
let reqObj = {
|
|
2003
|
+
url: 'https://www.mlb.com/live-stream-games/big-inning',
|
|
2004
|
+
headers: {
|
|
2005
|
+
'User-Agent': USER_AGENT,
|
|
2006
|
+
'Origin': 'https://www.mlb.com',
|
|
2007
|
+
'Referer': 'https://www.mlb.com',
|
|
2008
|
+
'Accept-Encoding': 'gzip, deflate, br'
|
|
2009
|
+
},
|
|
2010
|
+
gzip: true
|
|
2011
|
+
}
|
|
2012
|
+
var response = await this.httpGet(reqObj)
|
|
2013
|
+
if ( response ) {
|
|
2014
|
+
// disabled because it's very big!
|
|
2015
|
+
//this.debuglog(response)
|
|
2016
|
+
// break HTML into array based on table rows
|
|
2017
|
+
var rows = response.split('<tr>')
|
|
2018
|
+
// start iterating at 2 (after header row)
|
|
2019
|
+
for (var i=2; i<rows.length; i++) {
|
|
2020
|
+
// split HTML row into array with columns
|
|
2021
|
+
let cols = rows[i].split('<td>')
|
|
2022
|
+
|
|
2023
|
+
// define some variables that persist for each row
|
|
2024
|
+
let parts
|
|
2025
|
+
let year
|
|
2026
|
+
let month
|
|
2027
|
+
let day
|
|
2028
|
+
let this_datestring
|
|
2029
|
+
let add_date = 0
|
|
2030
|
+
let d
|
|
2031
|
+
|
|
2032
|
+
for (var j=1; j<cols.length; j++) {
|
|
2033
|
+
// split on closing bracket to get column text at resulting array index 0
|
|
2034
|
+
let col = cols[j].split('<')
|
|
2035
|
+
switch(j){
|
|
2036
|
+
// first column is date
|
|
2037
|
+
case 1:
|
|
2038
|
+
// split date into array
|
|
2039
|
+
parts = col[0].split(' ')
|
|
2040
|
+
year = parts[2]
|
|
2041
|
+
// get month index, zero-based
|
|
2042
|
+
month = new Date(Date.parse(parts[0] +" 1, 2021")).getMonth()
|
|
2043
|
+
day = parts[1].substring(0,parts[1].length-3)
|
|
2044
|
+
this_datestring = new Date(year, month, day).toISOString().substring(0,10)
|
|
2045
|
+
this.cache.bigInningSchedule[this_datestring] = {}
|
|
2046
|
+
// increment month index (not zero-based)
|
|
2047
|
+
month += 1
|
|
2048
|
+
break
|
|
2049
|
+
// remaining columns are times
|
|
2050
|
+
default:
|
|
2051
|
+
let hour
|
|
2052
|
+
let minute = '00'
|
|
2053
|
+
let ampm
|
|
2054
|
+
// if time has colon, split into array on that to get hour and minute parts
|
|
2055
|
+
if ( col[0].indexOf(':') > 0 ) {
|
|
2056
|
+
parts = col[0].split(':')
|
|
2057
|
+
hour = parseInt(parts[0])
|
|
2058
|
+
minute = parts[1].substring(0,2)
|
|
2059
|
+
} else {
|
|
2060
|
+
hour = parseInt(col[0].substring(0,col[0].length-2))
|
|
2061
|
+
}
|
|
2062
|
+
ampm = col[0].substring(col[0].length-2,col[0].length)
|
|
2063
|
+
// convert hour to 24-hour format
|
|
2064
|
+
if ( (ampm == 'PM') || ((hour == 12) && (ampm == 'AM')) ) {
|
|
2065
|
+
hour += 12
|
|
2066
|
+
}
|
|
2067
|
+
// these times are EDT so add 4 for UTC
|
|
2068
|
+
hour += 4
|
|
2069
|
+
// if hour is beyond 23, note we will have to add 1 day
|
|
2070
|
+
if ( hour > 23 ) {
|
|
2071
|
+
add_date = 1
|
|
2072
|
+
hour -= 24
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
d = new Date(this_datestring + 'T' + hour.toString().padStart(2, '0') + ':' + minute.toString().padStart(2, '0') + ':00.000+00:00')
|
|
2076
|
+
d.setDate(d.getDate()+add_date)
|
|
2077
|
+
switch(j){
|
|
2078
|
+
// 2nd column is start time
|
|
2079
|
+
case 2:
|
|
2080
|
+
this.cache.bigInningSchedule[this_datestring].start = d
|
|
2081
|
+
break
|
|
2082
|
+
// 3rd column is end time
|
|
2083
|
+
case 3:
|
|
2084
|
+
this.cache.bigInningSchedule[this_datestring].end = d
|
|
2085
|
+
break
|
|
2086
|
+
}
|
|
2087
|
+
break
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
this.debuglog(JSON.stringify(this.cache.bigInningSchedule))
|
|
2092
|
+
|
|
2093
|
+
// Default cache period is 1 day from now
|
|
2094
|
+
let oneDayFromNow = new Date()
|
|
2095
|
+
oneDayFromNow.setDate(oneDayFromNow.getDate()+1)
|
|
2096
|
+
let cacheExpiry = oneDayFromNow
|
|
2097
|
+
this.cache.bigInningScheduleCacheExpiry = cacheExpiry
|
|
2098
|
+
|
|
2099
|
+
this.save_cache_data()
|
|
2100
|
+
} else {
|
|
2101
|
+
this.log('error : invalid response from url ' + getObj.url)
|
|
2102
|
+
}
|
|
2103
|
+
} else {
|
|
2104
|
+
this.debuglog('using cached big inning schedule')
|
|
2105
|
+
}
|
|
2106
|
+
// If we requested the schedule for a specific date, and it exists, return it
|
|
2107
|
+
if ( dateString ) {
|
|
2108
|
+
if ( this.cache.bigInningSchedule && this.cache.bigInningSchedule[dateString] ) {
|
|
2109
|
+
return this.cache.bigInningSchedule[dateString]
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
} catch(e) {
|
|
2113
|
+
this.log('getBigInningSchedule error : ' + e.message)
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// Get Big Inning URL, used to determine the stream URL if available
|
|
2118
|
+
async getBigInningURL() {
|
|
2119
|
+
try {
|
|
2120
|
+
this.debuglog('getBigInningURL')
|
|
2121
|
+
|
|
2122
|
+
let cache_data
|
|
2123
|
+
let currentDate = new Date()
|
|
2124
|
+
if ( !this.cache || !this.cache.bigInningURLCacheExpiry || (currentDate > new Date(this.cache.bigInningURLCacheExpiry)) ) {
|
|
2125
|
+
let reqObj = {
|
|
2126
|
+
url: 'https://dapi.cms.mlbinfra.com/v2/content/en-us/vsmcontents/live-now-mlb-big-inning',
|
|
2127
|
+
headers: {
|
|
2128
|
+
'User-Agent': USER_AGENT,
|
|
2129
|
+
'Origin': 'https://www.mlb.com',
|
|
2130
|
+
'Referer': 'https://www.mlb.com',
|
|
2131
|
+
'Content-Type': 'application/json',
|
|
2132
|
+
'Accept-Encoding': 'gzip, deflate, br'
|
|
2133
|
+
},
|
|
2134
|
+
gzip: true
|
|
2135
|
+
}
|
|
2136
|
+
var response = await this.httpGet(reqObj)
|
|
2137
|
+
if ( this.isValidJson(response) ) {
|
|
2138
|
+
this.debuglog(response)
|
|
2139
|
+
cache_data = JSON.parse(response)
|
|
2140
|
+
|
|
2141
|
+
// Default cache period is 1 day from now
|
|
2142
|
+
let oneDayFromNow = new Date()
|
|
2143
|
+
oneDayFromNow.setDate(oneDayFromNow.getDate()+1)
|
|
2144
|
+
let cacheExpiry = oneDayFromNow
|
|
2145
|
+
|
|
2146
|
+
let today = this.liveDate()
|
|
2147
|
+
|
|
2148
|
+
if ( cache_data.title && (cache_data.title == 'LIVE NOW: MLB Big Inning') ) {
|
|
2149
|
+
this.debuglog('active big inning url')
|
|
2150
|
+
this.cache.bigInningURL = cache_data.references.video[0].fields.url
|
|
2151
|
+
|
|
2152
|
+
if ( this.cache.bigInningSchedule[today] && (currentDate < this.cache.bigInningSchedule[today].end ) ) {
|
|
2153
|
+
let scheduledEnd = new Date(this.cache.bigInningSchedule[today].end)
|
|
2154
|
+
scheduledEnd.setHours(scheduledEnd.getHours()+1)
|
|
2155
|
+
this.debuglog('setting cache expiry to scheduled end plus 1 hour: ' + scheduledEnd)
|
|
2156
|
+
cacheExpiry = scheduledEnd
|
|
2157
|
+
} else {
|
|
2158
|
+
// if it's not in our schedule, we can find the end time by parsing the time from the slug text, then adding the duration
|
|
2159
|
+
let slug_array = cache_data.references.video[0].slug.split('-')
|
|
2160
|
+
let et_hour = parseInt(slug_array[0])
|
|
2161
|
+
let et_minute
|
|
2162
|
+
if ( slug_array[1].indexOf('pm') ) {
|
|
2163
|
+
et_minute = slug_array[1].replace('pm','')
|
|
2164
|
+
et_hour += 12
|
|
2165
|
+
if ( et_hour == 24 ) et_hour = 0
|
|
2166
|
+
} else {
|
|
2167
|
+
et_minute = slug_array[1].replace('am','')
|
|
2168
|
+
}
|
|
2169
|
+
let scheduledEnd = new Date(today + 'T' + et_hour.toString().padStart(2,'0') + ':' + et_minute.padStart(2,'0') + ':00.000-04:00')
|
|
2170
|
+
|
|
2171
|
+
let duration_array = cache_data.references.video[0].fields.duration.split(':')
|
|
2172
|
+
scheduledEnd.setHours(scheduledEnd.getHours()+(parseInt(duration_array[0])-1))
|
|
2173
|
+
scheduledEnd.setMinutes(scheduledEnd.getMinutes()+parseInt(duration_array[1]))
|
|
2174
|
+
|
|
2175
|
+
this.debuglog('setting cache expiry to duration: ' + scheduledEnd.toISOString())
|
|
2176
|
+
cacheExpiry = scheduledEnd
|
|
2177
|
+
}
|
|
2178
|
+
} else {
|
|
2179
|
+
this.debuglog('no active big inning url')
|
|
2180
|
+
this.cache.bigInningURL = ''
|
|
2181
|
+
// check when next big inning is scheduled to start, within 5 days
|
|
2182
|
+
let counter = 5
|
|
2183
|
+
let checkDate = today
|
|
2184
|
+
await this.getBigInningSchedule()
|
|
2185
|
+
while ( counter < 5 ) {
|
|
2186
|
+
if ( this.cache.bigInningSchedule[checkDate] && (currentDate < this.cache.bigInningSchedule[checkDate].start ) ) {
|
|
2187
|
+
this.debuglog('setting cache expiry to next scheduled start: ' + this.cache.bigInningSchedule[checkDate].start)
|
|
2188
|
+
cacheExpiry = this.cache.bigInningSchedule[checkDate].start
|
|
2189
|
+
break
|
|
2190
|
+
}
|
|
2191
|
+
checkDate = new Date(checkDate).setDate(checkDate.getDate()+1).toISOString().substring(0,10)
|
|
2192
|
+
counter++
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// finally save the setting
|
|
2197
|
+
this.cache.bigInningURLCacheExpiry = cacheExpiry
|
|
2198
|
+
this.save_cache_data()
|
|
2199
|
+
} else {
|
|
2200
|
+
this.log('error : invalid json from url ' + getObj.url)
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
if ( this.cache.bigInningURL != '' ) {
|
|
2204
|
+
return this.cache.bigInningURL
|
|
2205
|
+
}
|
|
2206
|
+
} catch(e) {
|
|
2207
|
+
this.log('getBigInningURL error : ' + e.message)
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// Get Big Inning stream URL
|
|
2212
|
+
async getBigInningStreamURL() {
|
|
2213
|
+
this.debuglog('getBigInningStreamURL')
|
|
2214
|
+
if ( this.cache.bigInningStreamURL && this.cache.bigInningURLExpiry && (currentDate < new Date(this.cache.bigInningURLCacheExpiry)) ) {
|
|
2215
|
+
this.log('using cached bigInningStreamURL')
|
|
2216
|
+
return this.cache.bigInningStreamURL
|
|
2217
|
+
} else {
|
|
2218
|
+
var playbackURL = await this.getBigInningURL()
|
|
2219
|
+
if ( !playbackURL ) {
|
|
2220
|
+
this.debuglog('no active big inning url')
|
|
2221
|
+
} else {
|
|
2222
|
+
this.debuglog('getBigInningStreamURL from ' + playbackURL)
|
|
2223
|
+
let reqObj = {
|
|
2224
|
+
url: playbackURL,
|
|
2225
|
+
simple: false,
|
|
2226
|
+
headers: {
|
|
2227
|
+
'Authorization': 'Bearer ' + await this.getOktaAccessToken() || this.halt('missing OktaAccessToken'),
|
|
2228
|
+
'User-agent': USER_AGENT,
|
|
2229
|
+
'Accept': '*/*',
|
|
2230
|
+
'Origin': 'https://www.mlb.com',
|
|
2231
|
+
'Referer': 'https://www.mlb.com/',
|
|
2232
|
+
'Accept-Encoding': 'gzip, deflate, br'
|
|
2233
|
+
},
|
|
2234
|
+
gzip: true
|
|
2235
|
+
}
|
|
2236
|
+
var response = await this.httpGet(reqObj)
|
|
2237
|
+
if ( this.isValidJson(response) ) {
|
|
2238
|
+
this.debuglog('getBigInningStreamURL response : ' + response)
|
|
2239
|
+
let obj = JSON.parse(response)
|
|
2240
|
+
if ( obj.success && (obj.success == true) ) {
|
|
2241
|
+
this.debuglog('found bigInningStreamURL : ' + obj.data[0].value)
|
|
2242
|
+
this.cache.bigInningStreamURL = obj.data[0].value
|
|
2243
|
+
this.save_cache_data()
|
|
2244
|
+
return this.cache.bigInningStreamURL
|
|
2245
|
+
} else {
|
|
2246
|
+
this.log('getBigInningStreamURL error')
|
|
2247
|
+
this.log(obj.errorCode)
|
|
2248
|
+
this.log(obj.message)
|
|
2249
|
+
return
|
|
2250
|
+
}
|
|
2251
|
+
} else if ( response.startsWith('#EXTM3U') ) {
|
|
2252
|
+
this.debuglog('getBigInningStreamURL is bigInningURL : ' + playbackURL)
|
|
2253
|
+
this.cache.bigInningStreamURL = playbackURL
|
|
2254
|
+
this.save_cache_data()
|
|
2255
|
+
return this.cache.bigInningStreamURL
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
module.exports = sessionClass
|