mlbserver 2025.2.25 → 2025.3.2-9.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/Dockerfile +1 -1
- package/README.md +9 -50
- package/docker-cli +8 -0
- package/docker-compose.yml +3 -4
- package/index.js +94 -55
- package/mlbserver.subfolder.conf +20 -0
- package/package.json +1 -1
- package/session.js +42 -18
package/Dockerfile
CHANGED
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm install -g mlbserver
|
|
|
14
14
|
|
|
15
15
|
### docker
|
|
16
16
|
```
|
|
17
|
-
docker pull tonywagner/mlbserver
|
|
17
|
+
docker pull tonywagner/mlbserver:latest
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
|
|
@@ -30,54 +30,10 @@ node index.js
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
### docker-compose
|
|
33
|
-
Update the environment variables
|
|
34
|
-
```
|
|
35
|
-
services:
|
|
36
|
-
mlbserver:
|
|
37
|
-
image: tonywagner/mlbserver:latest
|
|
38
|
-
container_name: mlbserver
|
|
39
|
-
environment:
|
|
40
|
-
- TZ=America/New York
|
|
41
|
-
- data_directory=/mlbserver/data_directory
|
|
42
|
-
- account_username=your.account.email@example.com
|
|
43
|
-
- account_password=youraccountpassword
|
|
44
|
-
#- fav_teams=AZ,BAL
|
|
45
|
-
#- debug=false
|
|
46
|
-
#- port=9999
|
|
47
|
-
#- multiview_port=10000
|
|
48
|
-
#- multiview_path=
|
|
49
|
-
#- ffmpeg_path=
|
|
50
|
-
#- ffmpeg_encoder=
|
|
51
|
-
#- page_username=
|
|
52
|
-
#- page_password=
|
|
53
|
-
#- content_protect=
|
|
54
|
-
#- gamechanger_delay=0
|
|
55
|
-
#- http_root=/mlbserver
|
|
56
|
-
ports:
|
|
57
|
-
- 9999:9999
|
|
58
|
-
- 10000:10000
|
|
59
|
-
volumes:
|
|
60
|
-
- /path/to/your/desired/local/mlbserver/persistent/data/directory:/mlbserver/data_directory
|
|
61
|
-
```
|
|
62
|
-
Then launch it with ```docker-compose up --detach```
|
|
33
|
+
Update the required environment variables in the sample [docker-compose.yml](https://github.com/tonywagner/mlbserver/blob/master/docker-compose.yml) file, then launch it with ```docker-compose up --detach```
|
|
63
34
|
|
|
64
35
|
### docker-cli
|
|
65
|
-
Update the environment variables in the command
|
|
66
|
-
```
|
|
67
|
-
docker run -d \
|
|
68
|
-
--name mlbserver \
|
|
69
|
-
--env TZ="America/New_York" \
|
|
70
|
-
--env data_directory=/mlbserver/data_directory \
|
|
71
|
-
--env account_username=your.account.email@example.com \
|
|
72
|
-
--env account_password=youraccountpassword \
|
|
73
|
-
-p 9999:9999 \
|
|
74
|
-
-p 10000:10000 \
|
|
75
|
-
--volume /path/to/your/desired/local/mlbserver/persistent/data/directory:/mlbserver/data_directory \
|
|
76
|
-
tonywagner/mlbserver
|
|
77
|
-
```
|
|
78
|
-
Subsequent runs can be launched with ```docker start mlbserver```
|
|
79
|
-
|
|
80
|
-
Docker installs may require further configuration to get multiview streaming to work.
|
|
36
|
+
Update the environment variables in the sample [docker-cli](https://github.com/tonywagner/mlbserver/blob/master/docker-cli) command, then run the command. Subsequent runs can be launched with ```docker start mlbserver```
|
|
81
37
|
|
|
82
38
|
|
|
83
39
|
## Usage
|
|
@@ -93,7 +49,7 @@ Basic command line or Docker environment options:
|
|
|
93
49
|
--logout or -l (logs out and clears session)
|
|
94
50
|
--session or -s (clears session)
|
|
95
51
|
--cache or -c (clears cache)
|
|
96
|
-
--env or -e (use environment variables instead of command line arguments;
|
|
52
|
+
--env or -e (use environment variables instead of command line arguments; automatically applied in the Docker image)
|
|
97
53
|
```
|
|
98
54
|
|
|
99
55
|
Advanced command line or Docker environment options:
|
|
@@ -102,8 +58,9 @@ Advanced command line or Docker environment options:
|
|
|
102
58
|
--account_username (required, email address, default will use stored credentials or prompt user to enter them if not using Docker)
|
|
103
59
|
--account_password (required, default will use stored credentials or prompt user to enter them if not using Docker)
|
|
104
60
|
--fav_teams (optional, comma-separated list of favorite team abbreviations from https://github.com/tonywagner/mlbserver/blob/master/session.js#L23 -- will prompt if not set or stored and not using Docker)
|
|
61
|
+
--http_root (specify the alternative http webroot or initial path prefix, default is none)
|
|
105
62
|
--free (optional, highlights free games)
|
|
106
|
-
--multiview_port (port for multiview streaming; defaults to 1 more than primary port, or 10000)
|
|
63
|
+
--multiview_port (local port for multiview streaming; defaults to 1 more than primary port, or 10000; does not need to be mapped or used externally)
|
|
107
64
|
--multiview_path (where to create the folder for multiview encoded files; defaults to app directory)
|
|
108
65
|
--ffmpeg_path (path to ffmpeg binary to use for multiview encoding; default downloads a binary using ffmpeg-static)
|
|
109
66
|
--ffmpeg_encoder (ffmpeg video encoder to use for multiview; default is the software encoder libx264)
|
|
@@ -112,9 +69,11 @@ Advanced command line or Docker environment options:
|
|
|
112
69
|
--page_password (password to protect pages; default is no protection)
|
|
113
70
|
--content_protect (specify the content protection key to include as a URL parameter, if page protection is enabled)
|
|
114
71
|
--gamechanger_delay (specify extra delay for the gamechanger switches in 10 second increments, default is 0)
|
|
115
|
-
--
|
|
72
|
+
--data_directory (defaults to installed application directory; in the Docker image, this defaults to /mlbserver/data_directory for mapping persistent storage)
|
|
116
73
|
```
|
|
117
74
|
|
|
75
|
+
Supports [SWAG](https://docs.linuxserver.io/general/swag/#preset-proxy-confs) using the custom [mlbserver.subfolder.conf](https://github.com/tonywagner/mlbserver/blob/master/mlbserver.subfolder.conf) file.
|
|
76
|
+
|
|
118
77
|
For multiview, the default software encoder is limited by your CPU. You may want to experiment with different ffmpeg hardware encoders. "h264_videotoolbox" is confirmed to work on supported Macs, and "h264_v4l2m2m" is confirmed to work on a Raspberry Pi 4 (and likely other Linux systems) when ffmpeg is compiled with this patch: https://www.raspberrypi.org/forums/viewtopic.php?p=1780625#p1780625
|
|
119
78
|
|
|
120
79
|
More potential hardware encoders are described at https://stackoverflow.com/a/50703794
|
package/docker-cli
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
docker run -d \
|
|
2
|
+
--name mlbserver \
|
|
3
|
+
--env TZ="America/New_York" \
|
|
4
|
+
--env account_username=your.account.email@example.com \
|
|
5
|
+
--env account_password=youraccountpassword \
|
|
6
|
+
-p 9999:9999 \
|
|
7
|
+
--volume /path/to/your/desired/local/mlbserver/persistent/data/directory:/mlbserver/data_directory \
|
|
8
|
+
tonywagner/mlbserver
|
package/docker-compose.yml
CHANGED
|
@@ -4,13 +4,11 @@ services:
|
|
|
4
4
|
container_name: mlbserver
|
|
5
5
|
environment:
|
|
6
6
|
- TZ=America/New York
|
|
7
|
-
- data_directory=/mlbserver/data_directory
|
|
8
7
|
- account_username=your.account.email@example.com
|
|
9
8
|
- account_password=youraccountpassword
|
|
10
9
|
#- fav_teams=AZ,BAL
|
|
10
|
+
#- http_root=/mlbserver
|
|
11
11
|
#- debug=false
|
|
12
|
-
#- port=9999
|
|
13
|
-
#- multiview_port=10000
|
|
14
12
|
#- multiview_path=
|
|
15
13
|
#- ffmpeg_path=
|
|
16
14
|
#- ffmpeg_encoder=
|
|
@@ -18,8 +16,9 @@ services:
|
|
|
18
16
|
#- page_password=
|
|
19
17
|
#- content_protect=
|
|
20
18
|
#- gamechanger_delay=0
|
|
19
|
+
#- PUID=1000
|
|
20
|
+
#- PGID=1000
|
|
21
21
|
ports:
|
|
22
22
|
- 9999:9999
|
|
23
|
-
- 10000:10000
|
|
24
23
|
volumes:
|
|
25
24
|
- /path/to/your/desired/local/mlbserver/persistent/data/directory:/mlbserver/data_directory
|
package/index.js
CHANGED
|
@@ -29,10 +29,10 @@ const VALID_CONTROLS = [ 'Show', 'Hide' ]
|
|
|
29
29
|
const VALID_INNING_HALF = [ '', 'top', 'bottom' ]
|
|
30
30
|
const VALID_INNING_NUMBER = [ '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12' ]
|
|
31
31
|
const VALID_SCORES = [ 'Hide', 'Show' ]
|
|
32
|
-
const VALID_RESOLUTIONS = [ 'adaptive', '1080p60', '720p60', '720p', '540p', '504p', '360p', '288p', '
|
|
32
|
+
const VALID_RESOLUTIONS = [ 'adaptive', '1080p60', '720p60', '720p', '540p', '504p', '360p', '288p', '216p', '180p', 'none' ]
|
|
33
33
|
const DEFAULT_MULTIVIEW_RESOLUTION = '504p'
|
|
34
34
|
// Corresponding andwidths to display for above resolutions
|
|
35
|
-
const DISPLAY_BANDWIDTHS = [ '', '9600k', '6600k', '4160k', '2950k', '2120k', '1400k', '1000k', '600k', '' ]
|
|
35
|
+
const DISPLAY_BANDWIDTHS = [ '', '9600k', '6600k', '4160k', '2950k', '2120k', '1400k', '1000k', '600k', '300k', '' ]
|
|
36
36
|
const VALID_AUDIO_TRACKS = [ 'all', 'English', 'Home Radio', 'Casa Radio', 'Away Radio', 'Visita Radio', 'Park', 'none' ]
|
|
37
37
|
const DISPLAY_AUDIO_TRACKS = [ 'all', 'TV', 'Radio', 'Spa.', 'Away Rad.', 'Away Sp.', 'Park', 'none' ]
|
|
38
38
|
const DEFAULT_MULTIVIEW_AUDIO_TRACK = 'English'
|
|
@@ -56,21 +56,42 @@ const GAMECHANGER_RESOLUTIONS = {
|
|
|
56
56
|
'frame_rate': '29.97',
|
|
57
57
|
'url_bandwidth': '1800',
|
|
58
58
|
'bandwidth': '2120',
|
|
59
|
-
'codec': '
|
|
59
|
+
'codec': '4d401f'
|
|
60
|
+
},
|
|
61
|
+
'180p': {
|
|
62
|
+
'resolution': '320x180',
|
|
63
|
+
'frame_rate': '29.97',
|
|
64
|
+
'url_bandwidth': '192',
|
|
65
|
+
'bandwidth': '600',
|
|
66
|
+
'codec': '4d401f'
|
|
67
|
+
},
|
|
68
|
+
'216p': {
|
|
69
|
+
'resolution': '384x216',
|
|
70
|
+
'frame_rate': '29.97',
|
|
71
|
+
'url_bandwidth': '450',
|
|
72
|
+
'bandwidth': '600',
|
|
73
|
+
'codec': '4d401f'
|
|
74
|
+
},
|
|
75
|
+
'288p': {
|
|
76
|
+
'resolution': '512x288',
|
|
77
|
+
'frame_rate': '29.97',
|
|
78
|
+
'url_bandwidth': '800',
|
|
79
|
+
'bandwidth': '1000',
|
|
80
|
+
'codec': '4d401f'
|
|
60
81
|
},
|
|
61
82
|
'360p': {
|
|
62
83
|
'resolution': '640x360',
|
|
63
84
|
'frame_rate': '29.97',
|
|
64
85
|
'url_bandwidth': '1200',
|
|
65
86
|
'bandwidth': '1400',
|
|
66
|
-
'codec': '
|
|
87
|
+
'codec': '4d401f'
|
|
67
88
|
},
|
|
68
89
|
'540p': {
|
|
69
90
|
'resolution': '960x540',
|
|
70
91
|
'frame_rate': '29.97',
|
|
71
92
|
'url_bandwidth': '2500',
|
|
72
93
|
'bandwidth': '2950',
|
|
73
|
-
'codec': '
|
|
94
|
+
'codec': '4d401f'
|
|
74
95
|
},
|
|
75
96
|
'720p': {
|
|
76
97
|
'resolution': '1280x720',
|
|
@@ -84,7 +105,14 @@ const GAMECHANGER_RESOLUTIONS = {
|
|
|
84
105
|
'frame_rate': '59.94',
|
|
85
106
|
'url_bandwidth': '5600',
|
|
86
107
|
'bandwidth': '6600',
|
|
87
|
-
'codec': '
|
|
108
|
+
'codec': '640029'
|
|
109
|
+
},
|
|
110
|
+
'1080p60': {
|
|
111
|
+
'resolution': '1920x1080',
|
|
112
|
+
'frame_rate': '59.94',
|
|
113
|
+
'url_bandwidth': '7500',
|
|
114
|
+
'bandwidth': '9600',
|
|
115
|
+
'codec': '64002a'
|
|
88
116
|
}
|
|
89
117
|
}
|
|
90
118
|
const GAMECHANGER_LIST_SIZE = 6
|
|
@@ -106,7 +134,12 @@ var argv = minimist(process.argv, {
|
|
|
106
134
|
string: ['account_username', 'account_password', 'fav_teams', 'multiview_path', 'ffmpeg_path', 'ffmpeg_encoder', 'page_username', 'page_password', 'content_protect', 'data_directory', 'http_root']
|
|
107
135
|
})
|
|
108
136
|
|
|
109
|
-
if (argv.env)
|
|
137
|
+
if (argv.env) {
|
|
138
|
+
process.env.port = argv.port
|
|
139
|
+
process.env.multiview_port = argv.multiview_port
|
|
140
|
+
process.env.data_directory = argv.data_directory
|
|
141
|
+
argv = process.env
|
|
142
|
+
}
|
|
110
143
|
|
|
111
144
|
// Version
|
|
112
145
|
var version = require('./package').version
|
|
@@ -500,7 +533,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
500
533
|
return line
|
|
501
534
|
} else if ( segment_found ) {
|
|
502
535
|
segment_found = false
|
|
503
|
-
return http_root + '/ts?url='+encodeURIComponent(url.resolve(streamURL, line.trim())) + content_protect + referer_parameter + token_parameter
|
|
536
|
+
return http_root + '/segment.ts?url='+encodeURIComponent(url.resolve(streamURL, line.trim())) + content_protect + referer_parameter + token_parameter
|
|
504
537
|
}
|
|
505
538
|
|
|
506
539
|
// Omit keyframe tracks
|
|
@@ -562,7 +595,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
562
595
|
//var parsed = line.match(/URI="([^"]+)"?$/)
|
|
563
596
|
var parsed = line.match(',URI="([^"]+)"')
|
|
564
597
|
if ( parsed[1] ) {
|
|
565
|
-
newurl = http_root + '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim()))
|
|
598
|
+
newurl = http_root + '/playlist.m3u8?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim()))
|
|
566
599
|
if ( force_vod != VALID_FORCE_VOD[0] ) newurl += '&force_vod=on'
|
|
567
600
|
if ( inning_half != VALID_INNING_HALF[0] ) newurl += '&inning_half=' + inning_half
|
|
568
601
|
if ( inning_number != VALID_INNING_NUMBER[0] ) newurl += '&inning_number=' + inning_number
|
|
@@ -595,7 +628,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
595
628
|
if ( resolution == VALID_RESOLUTIONS[VALID_RESOLUTIONS.length-1] ) {
|
|
596
629
|
return
|
|
597
630
|
} else if ( resolution === VALID_RESOLUTIONS[0] ) {
|
|
598
|
-
|
|
631
|
+
return line
|
|
599
632
|
} else {
|
|
600
633
|
if ( (video_track_found == false) && (line.indexOf(resolution+',FRAME-RATE='+frame_rate) > 0) ) {
|
|
601
634
|
video_track_matched = true
|
|
@@ -616,7 +649,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
616
649
|
if ( line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="') ) {
|
|
617
650
|
var parsed = line.match(',URI="([^"]+)"')
|
|
618
651
|
if ( parsed[1] ) {
|
|
619
|
-
newurl = http_root + '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim())) + content_protect + referer_parameter + token_parameter
|
|
652
|
+
newurl = http_root + '/playlist.m3u8?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim())) + content_protect + referer_parameter + token_parameter
|
|
620
653
|
return '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="' + newurl + '"'
|
|
621
654
|
}
|
|
622
655
|
return
|
|
@@ -638,7 +671,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
638
671
|
if ( gamePk ) newurl += '&gamePk=' + gamePk
|
|
639
672
|
if ( audio_track != VALID_AUDIO_TRACKS[0] ) newurl += '&audio_track=' + encodeURIComponent(audio_track)
|
|
640
673
|
newurl += content_protect + referer_parameter + token_parameter
|
|
641
|
-
return http_root + '/playlist?url='+newurl
|
|
674
|
+
return http_root + '/playlist.m3u8?url='+newurl
|
|
642
675
|
}
|
|
643
676
|
})
|
|
644
677
|
.filter(function(line) {
|
|
@@ -661,21 +694,21 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
661
694
|
|
|
662
695
|
|
|
663
696
|
// Listen for playlist requests
|
|
664
|
-
app.get('/playlist', async function(req, res) {
|
|
697
|
+
app.get('/playlist.m3u8', async function(req, res) {
|
|
665
698
|
if ( ! (await protect(req, res)) ) return
|
|
666
699
|
|
|
667
|
-
session.requestlog('playlist', req, true)
|
|
700
|
+
session.requestlog('playlist.m3u8', req, true)
|
|
668
701
|
|
|
669
702
|
delete req.headers.host
|
|
670
703
|
|
|
671
704
|
var u = req.query.url
|
|
672
|
-
session.debuglog('playlist url : ' + u)
|
|
705
|
+
session.debuglog('playlist.m3u8 url : ' + u)
|
|
673
706
|
|
|
674
707
|
var referer = false
|
|
675
708
|
var referer_parameter = ''
|
|
676
709
|
if ( req.query.referer ) {
|
|
677
710
|
referer = decodeURIComponent(req.query.referer)
|
|
678
|
-
session.debuglog('found playlist referer : ' + referer)
|
|
711
|
+
session.debuglog('found playlist.m3u8 referer : ' + referer)
|
|
679
712
|
referer_parameter = '&referer=' + encodeURIComponent(req.query.referer)
|
|
680
713
|
}
|
|
681
714
|
|
|
@@ -810,9 +843,9 @@ app.get('/playlist', async function(req, res) {
|
|
|
810
843
|
|
|
811
844
|
if (line[0] === '#') return line
|
|
812
845
|
|
|
813
|
-
let newline = http_root + '/ts'
|
|
846
|
+
let newline = http_root + '/segment.ts'
|
|
814
847
|
if ( line.includes('.vtt') ) {
|
|
815
|
-
newline = http_root + '/vtt'
|
|
848
|
+
newline = http_root + '/subtitles.vtt'
|
|
816
849
|
}
|
|
817
850
|
|
|
818
851
|
newline += '?url='+encodeURIComponent(url.resolve(u, line.trim())) + content_protect + referer_parameter + token_parameter
|
|
@@ -820,7 +853,7 @@ app.get('/playlist', async function(req, res) {
|
|
|
820
853
|
|
|
821
854
|
// if an alternate audio track is specified, force removal of embedded audio
|
|
822
855
|
if ( (audio_track != VALID_AUDIO_TRACKS[0]) && (audio_track != VALID_AUDIO_TRACKS[1]) ) {
|
|
823
|
-
newline = 'download.
|
|
856
|
+
newline = http_root + '/download.ts?audio_track=' + encodeURIComponent(audio_track) + '&src=' + encodeURIComponent('http://127.0.0.1:' + session.data.port + newline) + content_protect
|
|
824
857
|
}
|
|
825
858
|
|
|
826
859
|
return newline
|
|
@@ -864,20 +897,20 @@ app.get('/playlist', async function(req, res) {
|
|
|
864
897
|
})
|
|
865
898
|
|
|
866
899
|
// Listen for ts requests (video segments) and decode them
|
|
867
|
-
app.get('/ts', async function(req, res) {
|
|
900
|
+
app.get('/segment.ts', async function(req, res) {
|
|
868
901
|
if ( ! (await protect(req, res)) ) return
|
|
869
902
|
|
|
870
|
-
session.requestlog('ts', req, true)
|
|
903
|
+
session.requestlog('segment.ts', req, true)
|
|
871
904
|
|
|
872
905
|
delete req.headers.host
|
|
873
906
|
|
|
874
907
|
var u = req.query.url
|
|
875
|
-
session.debuglog('ts url : ' + u)
|
|
908
|
+
session.debuglog('segment.ts url : ' + u)
|
|
876
909
|
|
|
877
910
|
var headers = {encoding:null}
|
|
878
911
|
|
|
879
912
|
if ( req.query.referer ) {
|
|
880
|
-
session.debuglog('found segment referer : ' + req.query.referer)
|
|
913
|
+
session.debuglog('found segment.ts referer : ' + req.query.referer)
|
|
881
914
|
referer = decodeURIComponent(req.query.referer)
|
|
882
915
|
headers.referer = referer
|
|
883
916
|
headers.origin = getOriginFromURL(referer)
|
|
@@ -926,20 +959,20 @@ app.get('/ts', async function(req, res) {
|
|
|
926
959
|
|
|
927
960
|
|
|
928
961
|
// Listen for WebVTT subtitle requests
|
|
929
|
-
app.get('/vtt', async function(req, res) {
|
|
962
|
+
app.get('/subtitles.vtt', async function(req, res) {
|
|
930
963
|
if ( ! (await protect(req, res)) ) return
|
|
931
964
|
|
|
932
|
-
session.requestlog('vtt', req, true)
|
|
965
|
+
session.requestlog('subtitles.vtt', req, true)
|
|
933
966
|
|
|
934
967
|
delete req.headers.host
|
|
935
968
|
|
|
936
969
|
var u = req.query.url
|
|
937
|
-
session.debuglog('vtt url : ' + u)
|
|
970
|
+
session.debuglog('subtitles.vtt url : ' + u)
|
|
938
971
|
|
|
939
972
|
var referer = false
|
|
940
973
|
if ( req.query.referer ) {
|
|
941
974
|
referer = decodeURIComponent(req.query.referer)
|
|
942
|
-
session.debuglog('found vtt referer : ' + referer)
|
|
975
|
+
session.debuglog('found subtitles.vtt referer : ' + referer)
|
|
943
976
|
}
|
|
944
977
|
|
|
945
978
|
var req = function () {
|
|
@@ -1168,7 +1201,7 @@ app.get('/gamechangerplaylist', async function(req, res) {
|
|
|
1168
1201
|
if ( session.temp_cache.gamechanger[id].segments[i].discontinuity ) {
|
|
1169
1202
|
session.temp_cache.gamechanger[id].playlist[resolution] += '#EXT-X-DISCONTINUITY' + '\n'
|
|
1170
1203
|
}
|
|
1171
|
-
session.temp_cache.gamechanger[id].playlist[resolution] += session.temp_cache.gamechanger[id].segments[i].extinf + '\n' + '/ts?url=' + encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].ts) + '&streamURLToken='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].streamURLToken) + content_protect + '\n'
|
|
1204
|
+
session.temp_cache.gamechanger[id].playlist[resolution] += session.temp_cache.gamechanger[id].segments[i].extinf + '\n' + '/segment.ts?url=' + encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].ts) + '&streamURLToken='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].streamURLToken) + content_protect + '\n'
|
|
1172
1205
|
}
|
|
1173
1206
|
|
|
1174
1207
|
session.debuglog(game_changer_title + 'playlist ' + session.temp_cache.gamechanger[id].playlist[resolution])
|
|
@@ -1518,6 +1551,8 @@ app.get('/', async function(req, res) {
|
|
|
1518
1551
|
let link = linkType.toLowerCase() + '.html'
|
|
1519
1552
|
if ( linkType == VALID_LINK_TYPES[1] ) {
|
|
1520
1553
|
link = linkType.toLowerCase() + '.m3u8'
|
|
1554
|
+
} else if ( linkType == VALID_LINK_TYPES[4] ) {
|
|
1555
|
+
link = linkType.toLowerCase() + '.ts'
|
|
1521
1556
|
} else {
|
|
1522
1557
|
force_vod = VALID_FORCE_VOD[0]
|
|
1523
1558
|
}
|
|
@@ -1569,7 +1604,7 @@ app.get('/', async function(req, res) {
|
|
|
1569
1604
|
body += '</td></tr>' + "\n"
|
|
1570
1605
|
}*/
|
|
1571
1606
|
|
|
1572
|
-
if (
|
|
1607
|
+
if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
|
|
1573
1608
|
blackouts = await session.get_blackout_games(cache_data.dates[0].date, true)
|
|
1574
1609
|
}
|
|
1575
1610
|
|
|
@@ -1681,6 +1716,7 @@ app.get('/', async function(req, res) {
|
|
|
1681
1716
|
} else {
|
|
1682
1717
|
teams += hometeam
|
|
1683
1718
|
}
|
|
1719
|
+
let filename_teams = awayteam + " @ " + hometeam
|
|
1684
1720
|
let pitchers = ""
|
|
1685
1721
|
let state = "<br/>"
|
|
1686
1722
|
|
|
@@ -1759,7 +1795,7 @@ app.get('/', async function(req, res) {
|
|
|
1759
1795
|
state += "<br/>" + detailedState
|
|
1760
1796
|
}
|
|
1761
1797
|
|
|
1762
|
-
var filename = gameDate + ' ' +
|
|
1798
|
+
var filename = gameDate + ' ' + filename_teams + ' '
|
|
1763
1799
|
|
|
1764
1800
|
if ( cache_data.dates[0].games[j].doubleHeader != 'N' ) {
|
|
1765
1801
|
state += "<br/>Game " + cache_data.dates[0].games[j].gameNumber
|
|
@@ -1839,7 +1875,7 @@ app.get('/', async function(req, res) {
|
|
|
1839
1875
|
body += '><td>' + description + teams + pitchers + state + '</td>'
|
|
1840
1876
|
|
|
1841
1877
|
// Check if Winter League / MiLB game first
|
|
1842
|
-
if ( (cache_data.dates[0].games[j].teams['home'].team.sport.id != levels['MLB']) && (mediaType == 'MLBTV') ) {
|
|
1878
|
+
if ( (cache_data.dates[0].games[j].teams['away'].team.sport.id != levels['MLB']) && (cache_data.dates[0].games[j].teams['home'].team.sport.id != levels['MLB']) && (mediaType == 'MLBTV') ) {
|
|
1843
1879
|
body += "<td>"
|
|
1844
1880
|
if ( cache_data.dates[0].games[j].broadcasts ) {
|
|
1845
1881
|
let broadcastName = 'N/A'
|
|
@@ -1935,9 +1971,12 @@ app.get('/', async function(req, res) {
|
|
|
1935
1971
|
|
|
1936
1972
|
// display blackout tooltip, if necessary
|
|
1937
1973
|
if ( blackouts[gamePk] ) {
|
|
1938
|
-
body += '<span class="tooltip"><span class="blackout">' + teamabbr + '</span><span class="tooltiptext">' + blackouts[gamePk].blackout_type
|
|
1939
|
-
if ( blackouts[gamePk].
|
|
1940
|
-
body += '
|
|
1974
|
+
body += '<span class="tooltip"><span class="blackout">' + teamabbr + '</span><span class="tooltiptext">' + blackouts[gamePk].blackout_type
|
|
1975
|
+
if ( blackouts[gamePk].blackout_type != 'Not entitled' ) {
|
|
1976
|
+
body += ' video blackout until approx. 2.5 hours after the game'
|
|
1977
|
+
if ( blackouts[gamePk].blackoutExpiry ) {
|
|
1978
|
+
body += ' (~' + blackouts[gamePk].blackoutExpiry.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ')'
|
|
1979
|
+
}
|
|
1941
1980
|
}
|
|
1942
1981
|
body += '</span></span>'
|
|
1943
1982
|
} else {
|
|
@@ -2054,7 +2093,7 @@ app.get('/', async function(req, res) {
|
|
|
2054
2093
|
body += "</table>" + "\n"
|
|
2055
2094
|
|
|
2056
2095
|
if ( (Object.keys(blackouts).length > 0) ) {
|
|
2057
|
-
body += '<span class="tooltip tinytext"><span class="blackout">strikethrough</span> indicates a live blackout<span class="tooltiptext">Tap or hover over the team abbreviation to see an estimate of when the blackout will be lifted (officially ~90 minutes, but more likely ~150 minutes or ~2.5 hours after the game ends).</span></span>' + "\n"
|
|
2096
|
+
body += '<span class="tooltip tinytext"><span class="blackout">strikethrough</span> indicates a live blackout or non-entitled video<span class="tooltiptext">Tap or hover over the team abbreviation to see an estimate of when the blackout will be lifted (officially ~90 minutes, but more likely ~150 minutes or ~2.5 hours after the game ends).</span></span>' + "\n"
|
|
2058
2097
|
if ( argv.free ) {
|
|
2059
2098
|
body += '<br/>'
|
|
2060
2099
|
}
|
|
@@ -2127,7 +2166,7 @@ app.get('/', async function(req, res) {
|
|
|
2127
2166
|
body += '<input type="checkbox" id="reencode"/> <span class="tooltip">Re-encode all audio<span class="tooltiptext">Uses more CPU. Generally only necessary if you need the multiview stream to continue after one of the individual streams has ended. (Any streams with sync adjustments above will automatically be re-encoded, regardless of this setting.)</span></span><br/>' + "\n"
|
|
2128
2167
|
body += '<input type="checkbox" id="park_audio"/> <span class="tooltip">Park audio: filter out announcers<span class="tooltiptext">Implies re-encoding all audio. If this is enabled, an extra audio filter is applied to remove the announcer voices.</span></span><br/>' + "\n"
|
|
2129
2168
|
body += '<hr><span class="tooltip">Alternate audio URL and sync<span class="tooltiptext">Optional: you can also include a separate audio-only URL as an additional alternate audio track. Archive games will likely require a very large negative sync value, as the radio broadcasts may not be trimmed like the video archives.</span></span>:<br/><textarea id="audio_url" rows=2 cols=60 oninput="this.value=stream_substitution(this.value)"></textarea><input id="audio_url_seek" type="number" value="0" style="vertical-align:top;font-size:.8em;width:4em"/>'
|
|
2130
|
-
body += '<hr>Watch: <a href="' + http_root + '/embed.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Embed</a> | <a href="' + http_root + '/stream.m3u8?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Stream</a> | <a href="' + http_root + '/chromecast.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Chromecast</a> | <a href="' + http_root + '/advanced.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Advanced</a> | <a href="' + http_root + '/download.
|
|
2169
|
+
body += '<hr>Watch: <a href="' + http_root + '/embed.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Embed</a> | <a href="' + http_root + '/stream.m3u8?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Stream</a> | <a href="' + http_root + '/chromecast.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Chromecast</a> | <a href="' + http_root + '/advanced.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Advanced</a> | <a href="' + http_root + '/download.ts?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '&filename=' + gameDate + ' Multiview">Download</a><br/><span class="tinytext">Kodi STRM files: <a href="' + http_root + '/kodi.strm?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Matrix/19+</a> (<a href="' + http_root + '/kodi.strm?version=18&src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Leia/18</a>)</span>'
|
|
2131
2170
|
body += '</td></tr></table><br/>' + "\n"
|
|
2132
2171
|
}
|
|
2133
2172
|
|
|
@@ -2983,18 +3022,18 @@ app.get('/kodi.strm', async function(req, res) {
|
|
|
2983
3022
|
})
|
|
2984
3023
|
|
|
2985
3024
|
// Listen for download requests (either for actual downloads or just proxying stream through ffmpeg)
|
|
2986
|
-
app.get('/download.
|
|
3025
|
+
app.get('/download.ts', async function(req, res) {
|
|
2987
3026
|
if ( ! (await protect(req, res)) ) return
|
|
2988
3027
|
|
|
2989
3028
|
try {
|
|
2990
3029
|
// we'll know it's an actual download request if it include a filename parameter
|
|
2991
3030
|
if ( req.query.filename ) {
|
|
2992
|
-
session.requestlog('download', req)
|
|
3031
|
+
session.requestlog('download.ts', req)
|
|
2993
3032
|
} else {
|
|
2994
3033
|
session.debuglog('force alternate audio', req)
|
|
2995
3034
|
}
|
|
2996
3035
|
|
|
2997
|
-
let server =
|
|
3036
|
+
let server = 'http://127.0.0.1:' + session.data.port + http_root
|
|
2998
3037
|
|
|
2999
3038
|
let video_url = '/stream.m3u8'
|
|
3000
3039
|
if ( req.query.src ) {
|
|
@@ -3006,7 +3045,7 @@ app.get('/download.html', async function(req, res) {
|
|
|
3006
3045
|
}
|
|
3007
3046
|
video_url = server + video_url
|
|
3008
3047
|
}
|
|
3009
|
-
session.debuglog('download src : ' + video_url)
|
|
3048
|
+
session.debuglog('download.ts src : ' + video_url)
|
|
3010
3049
|
|
|
3011
3050
|
// force adaptive streams to just use a single video resolution/track
|
|
3012
3051
|
if ( req.query.filename ) {
|
|
@@ -3022,13 +3061,13 @@ app.get('/download.html', async function(req, res) {
|
|
|
3022
3061
|
// Set input stream and minimize ffmpeg startup latency
|
|
3023
3062
|
ffmpeg_command.input(video_url)
|
|
3024
3063
|
.addInputOption('-fflags', 'nobuffer')
|
|
3025
|
-
.addInputOption('-probesize', '
|
|
3064
|
+
.addInputOption('-probesize', '1000000')
|
|
3026
3065
|
.addInputOption('-analyzeduration', '0')
|
|
3027
3066
|
|
|
3028
3067
|
// video
|
|
3029
3068
|
if ( !req.query.resolution || (req.query.resolution != VALID_RESOLUTIONS[VALID_RESOLUTIONS.length-1]) ) {
|
|
3030
3069
|
// copy first video track if available
|
|
3031
|
-
ffmpeg_command.addOutputOption('-map', '0:v:0
|
|
3070
|
+
ffmpeg_command.addOutputOption('-map', '0:v:0')
|
|
3032
3071
|
.addOutputOption('-c:v', 'copy')
|
|
3033
3072
|
} else {
|
|
3034
3073
|
// suppress video is "none" resolution was specified
|
|
@@ -3053,13 +3092,6 @@ app.get('/download.html', async function(req, res) {
|
|
|
3053
3092
|
ffmpeg_command.addOutputOption('-map', '[out0]')
|
|
3054
3093
|
.addOutputOption('-c:a', 'aac')
|
|
3055
3094
|
.addOutputOption('-ac:a:0', '1')
|
|
3056
|
-
|
|
3057
|
-
// if not downloading to a file, also copy source PTS values for continuous playback
|
|
3058
|
-
if ( !req.query.filename ) {
|
|
3059
|
-
ffmpeg_command.addOutputOption('-copyts')
|
|
3060
|
-
.addOutputOption('-muxpreload', '0')
|
|
3061
|
-
.addOutputOption('-muxdelay', '0')
|
|
3062
|
-
}
|
|
3063
3095
|
} else if ( (!req.query.filename && (req.query.audio_track != VALID_AUDIO_TRACKS[1])) || (req.query.audio_track == VALID_AUDIO_TRACKS[7]) ) {
|
|
3064
3096
|
// if we're not downloading a file, and we requested an alternate audio track, then we want to suppress the embedded TV audio
|
|
3065
3097
|
// or if the user requested no audio tracks in their download, we will suppress all
|
|
@@ -3071,22 +3103,29 @@ app.get('/download.html', async function(req, res) {
|
|
|
3071
3103
|
}
|
|
3072
3104
|
}
|
|
3073
3105
|
|
|
3106
|
+
// if not downloading to a file, also copy source PTS values for continuous playback
|
|
3107
|
+
if ( !req.query.filename ) {
|
|
3108
|
+
ffmpeg_command.addOutputOption('-copyts')
|
|
3109
|
+
.addOutputOption('-muxpreload', '0')
|
|
3110
|
+
.addOutputOption('-muxdelay', '0')
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3074
3113
|
// output mpegts to response stream
|
|
3075
3114
|
ffmpeg_command.addOutputOption('-f', 'mpegts')
|
|
3076
3115
|
.output(res)
|
|
3077
3116
|
.on('start', function(commandLine) {
|
|
3078
|
-
session.debuglog('download command started')
|
|
3117
|
+
session.debuglog('download.ts command started')
|
|
3079
3118
|
if ( argv.debug || argv.ffmpeg_logging ) {
|
|
3080
|
-
session.debuglog('download command: ' + commandLine)
|
|
3119
|
+
session.debuglog('download.ts command: ' + commandLine)
|
|
3081
3120
|
}
|
|
3082
3121
|
})
|
|
3083
3122
|
.on('error', function(err, stdout, stderr) {
|
|
3084
|
-
session.debuglog('download command stopped: ' + err.message)
|
|
3123
|
+
session.debuglog('download.ts command stopped: ' + err.message)
|
|
3085
3124
|
if ( stdout ) session.log(stdout)
|
|
3086
3125
|
if ( stderr ) session.log(stderr)
|
|
3087
3126
|
})
|
|
3088
3127
|
.on('end', function() {
|
|
3089
|
-
session.debuglog('download command ended')
|
|
3128
|
+
session.debuglog('download.ts command ended')
|
|
3090
3129
|
})
|
|
3091
3130
|
|
|
3092
3131
|
if ( argv.ffmpeg_logging ) {
|
|
@@ -3104,7 +3143,7 @@ app.get('/download.html', async function(req, res) {
|
|
|
3104
3143
|
|
|
3105
3144
|
ffmpeg_command.run()
|
|
3106
3145
|
} catch (e) {
|
|
3107
|
-
session.log('download request error : ' + e.message)
|
|
3146
|
+
session.log('download.ts request error : ' + e.message)
|
|
3108
3147
|
res.end('')
|
|
3109
3148
|
}
|
|
3110
|
-
})
|
|
3149
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
## Version 2025/02/27
|
|
2
|
+
# make sure that your mlbserver container is named mlbserver
|
|
3
|
+
# make sure that mlbserver is set to work with the base url /mlbserver/
|
|
4
|
+
#
|
|
5
|
+
# For the subfolder to work you need to edit your mlbserver docker compose / cli config
|
|
6
|
+
# and set the http_root environment variable to /mlbserver, e.g. in docker compose:
|
|
7
|
+
# - http_root=/mlbserver
|
|
8
|
+
|
|
9
|
+
location /mlbserver {
|
|
10
|
+
return 301 $scheme://$host/mlbserver/;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
location /mlbserver/ {
|
|
14
|
+
include /config/nginx/proxy.conf;
|
|
15
|
+
include /config/nginx/resolver.conf;
|
|
16
|
+
set $upstream_app mlbserver;
|
|
17
|
+
set $upstream_port 9999;
|
|
18
|
+
set $upstream_proto http;
|
|
19
|
+
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
|
|
20
|
+
}
|
package/package.json
CHANGED
package/session.js
CHANGED
|
@@ -38,10 +38,14 @@ const WINTER_LEAGUES = [AFL_ID, LIDOM_ID]
|
|
|
38
38
|
const BREAK_TYPES = ['Game Advisory', 'Pitching Substitution', 'Offensive Substitution', 'Defensive Sub', 'Defensive Switch', 'Runner Placed On Base']
|
|
39
39
|
// These are the events to keep, in addition to the last event of each at-bat, if we're skipping pitches
|
|
40
40
|
const ACTION_TYPES = ['Wild Pitch', 'Passed Ball', 'Stolen Base', 'Caught Stealing', 'Pickoff', 'Error', 'Out', 'Balk', 'Defensive Indiff', 'Other Advance']
|
|
41
|
+
// These are some idle events to skip
|
|
42
|
+
const IDLE_TYPES = ['Mound Visit', 'Batter Timeout', 'Pitcher Step Off']
|
|
41
43
|
const EVENT_START_PADDING = -3
|
|
42
|
-
const PITCH_END_PADDING =
|
|
44
|
+
const PITCH_END_PADDING = 2
|
|
43
45
|
const ACTION_END_PADDING = 7
|
|
44
46
|
const MINIMUM_BREAK_DURATION = 5
|
|
47
|
+
// extra padding for MLB events (2025)
|
|
48
|
+
const MLB_PADDING = 39
|
|
45
49
|
|
|
46
50
|
const LI_TABLE = {
|
|
47
51
|
1: {
|
|
@@ -1776,7 +1780,7 @@ class sessionClass {
|
|
|
1776
1780
|
if ( ((team.toUpperCase() == home_team) && (LEVELS[level.toUpperCase()] == home_level)) || ((team.toUpperCase() == away_team) && (LEVELS[level.toUpperCase()] == away_level)) || ((team.toUpperCase().indexOf('NATIONAL.') == 0) && (home_level == LEVELS['MLB'])) || ((team.toUpperCase().startsWith('FREE.') && cache_data.dates[0].games[j].broadcasts && cache_data.dates[0].games[j].broadcasts[0] && (cache_data.dates[0].games[j].broadcasts[0].freeGame == true))) ) {
|
|
1777
1781
|
|
|
1778
1782
|
// Check if Winter League / MiLB game first
|
|
1779
|
-
if ( (home_level != LEVELS['MLB']) && (mediaType == 'MLBTV') ) {
|
|
1783
|
+
if ( (away_level != LEVELS['MLB']) && (home_level != LEVELS['MLB']) && (mediaType == 'MLBTV') ) {
|
|
1780
1784
|
this.debuglog('matched non-MLB team for ' + cache_data.dates[0].games[j].teams['home'].team.abbreviation + '@' + cache_data.dates[0].games[j].teams['away'].team.abbreviation)
|
|
1781
1785
|
if ( cache_data.dates[0].games[j].broadcasts ) {
|
|
1782
1786
|
let broadcastName = 'N/A'
|
|
@@ -2892,8 +2896,14 @@ class sessionClass {
|
|
|
2892
2896
|
async getBroadcastStart(streamURL, streamURLToken) {
|
|
2893
2897
|
try {
|
|
2894
2898
|
this.debuglog('getBroadcastStart')
|
|
2899
|
+
|
|
2900
|
+
// MLB version
|
|
2901
|
+
let variant = '_5600K'
|
|
2902
|
+
if ( streamURL.includes('milb.com') ) {
|
|
2903
|
+
variant = '_1280x720_59_5472K'
|
|
2904
|
+
}
|
|
2895
2905
|
|
|
2896
|
-
let variant_url = 'http://localhost:' + this.data.port + '/playlist?url=' + encodeURIComponent(streamURL.substr(0,streamURL.length-5) + '
|
|
2906
|
+
let variant_url = 'http://localhost:' + this.data.port + '/playlist.m3u8?url=' + encodeURIComponent(streamURL.substr(0,streamURL.length-5) + variant + '.m3u8')
|
|
2897
2907
|
if ( streamURLToken ) {
|
|
2898
2908
|
variant_url += '&streamURLToken=' + encodeURIComponent(streamURLToken)
|
|
2899
2909
|
}
|
|
@@ -2929,6 +2939,15 @@ class sessionClass {
|
|
|
2929
2939
|
this.debuglog('getSkipMarkers')
|
|
2930
2940
|
|
|
2931
2941
|
if ( skip_adjust != 0 ) this.log('manual adjustment of ' + skip_adjust + ' seconds being applied')
|
|
2942
|
+
|
|
2943
|
+
let event_start_padding = EVENT_START_PADDING
|
|
2944
|
+
let pitch_end_padding = PITCH_END_PADDING
|
|
2945
|
+
let action_end_padding = ACTION_END_PADDING
|
|
2946
|
+
if ( !streamURL.includes('milb.com') ) {
|
|
2947
|
+
event_start_padding += MLB_PADDING
|
|
2948
|
+
pitch_end_padding += MLB_PADDING
|
|
2949
|
+
action_end_padding += MLB_PADDING
|
|
2950
|
+
}
|
|
2932
2951
|
|
|
2933
2952
|
if ( !this.temp_cache[gamePk] ) {
|
|
2934
2953
|
this.temp_cache[gamePk] = {}
|
|
@@ -2993,7 +3012,7 @@ class sessionClass {
|
|
|
2993
3012
|
if ((current_inning > start_inning) || ((current_inning == start_inning) && ((current_inning_half == start_inning_half) || (current_inning_half == 'bottom')))) {
|
|
2994
3013
|
// loop through events within each play
|
|
2995
3014
|
for (var j=0; j < cache_data.liveData.plays.allPlays[i].playEvents.length; j++) {
|
|
2996
|
-
let event_end_padding =
|
|
3015
|
+
let event_end_padding = action_end_padding
|
|
2997
3016
|
// always exclude break types
|
|
2998
3017
|
if (cache_data.liveData.plays.allPlays[i].playEvents[j].details && cache_data.liveData.plays.allPlays[i].playEvents[j].details.event && BREAK_TYPES.includes(cache_data.liveData.plays.allPlays[i].playEvents[j].details.event)) {
|
|
2999
3018
|
// if we're in the process of skipping inning breaks, treat the first break type we find as another inning break
|
|
@@ -3004,15 +3023,18 @@ class sessionClass {
|
|
|
3004
3023
|
continue
|
|
3005
3024
|
} else {
|
|
3006
3025
|
if ( (j < (cache_data.liveData.plays.allPlays[i].playEvents.length - 1)) && (!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))) ) {
|
|
3007
|
-
event_end_padding =
|
|
3026
|
+
event_end_padding = pitch_end_padding
|
|
3008
3027
|
}
|
|
3009
3028
|
let action_index
|
|
3010
|
-
// skip type 1 (breaks)
|
|
3011
|
-
if ((skip_type
|
|
3029
|
+
// skip type 1 (breaks) will look at all plays with an endTime
|
|
3030
|
+
if ((skip_type == 1) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime) {
|
|
3031
|
+
action_index = j
|
|
3032
|
+
// skip type 2 (idle time) will look at all non-idle plays with an endTime
|
|
3033
|
+
} else if ((skip_type == 2) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime && !IDLE_TYPES.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.description.includes(v))) {
|
|
3012
3034
|
action_index = j
|
|
3013
3035
|
} else if (skip_type == 3) {
|
|
3014
3036
|
// skip type 3 excludes non-action pitches (events that aren't last in the at-bat and don't fall under action types)
|
|
3015
|
-
if ( event_end_padding ==
|
|
3037
|
+
if ( event_end_padding == pitch_end_padding ) {
|
|
3016
3038
|
continue
|
|
3017
3039
|
} else {
|
|
3018
3040
|
// if the action is associated with another play or the event doesn't have an end time, use the previous event instead
|
|
@@ -3026,7 +3048,7 @@ class sessionClass {
|
|
|
3026
3048
|
if (typeof action_index === 'undefined') {
|
|
3027
3049
|
continue
|
|
3028
3050
|
} else {
|
|
3029
|
-
let break_end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].startTime) - broadcast_start_timestamp) / 1000) +
|
|
3051
|
+
let break_end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].startTime) - broadcast_start_timestamp) / 1000) + event_start_padding
|
|
3030
3052
|
|
|
3031
3053
|
// attempt to fix erroneous timestamps, like NYY-SEA 2022-08-09, bottom 11
|
|
3032
3054
|
if ( break_end < break_start ) {
|
|
@@ -3583,25 +3605,27 @@ class sessionClass {
|
|
|
3583
3605
|
let game = cache_data.results[j]
|
|
3584
3606
|
let game_pk = game.gamePk
|
|
3585
3607
|
this.debuglog('get_blackout_games checking game ' + game_pk)
|
|
3586
|
-
if ( game.blackedOutVideo ) {
|
|
3587
|
-
this.debuglog('get_blackout_games found blackout')
|
|
3608
|
+
if ( game.blackedOutVideo || !game.entitledVideo ) {
|
|
3609
|
+
this.debuglog('get_blackout_games found blackout or non-entitled video')
|
|
3588
3610
|
let blackout_type = ''
|
|
3589
|
-
|
|
3590
|
-
/*if ( game.videoStatusCodes.includes('2') ) {
|
|
3611
|
+
if ( game.videoStatusCodes.includes(2) ) {
|
|
3591
3612
|
this.debuglog('get_blackout_games found national blackout')
|
|
3592
3613
|
blackout_type = 'National/International'
|
|
3593
|
-
} else {
|
|
3614
|
+
} else if ( game.videoStatusCodes.includes(1) ) {
|
|
3594
3615
|
this.debuglog('get_blackout_games found local blackout')
|
|
3595
3616
|
blackout_type = 'Local'
|
|
3596
|
-
}
|
|
3617
|
+
} else {
|
|
3618
|
+
this.debuglog('get_blackout_games found non-entitled video')
|
|
3619
|
+
blackout_type = 'Not entitled'
|
|
3620
|
+
}
|
|
3597
3621
|
blackouts[game_pk] = { blackout_type: blackout_type }
|
|
3598
|
-
} else if ( !game.entitledVideo && (game.videoStatusCodes[0] == '3') ) {
|
|
3622
|
+
} /*else if ( !game.entitledVideo && (game.videoStatusCodes[0] == '3') ) {
|
|
3599
3623
|
this.debuglog('get_blackout_games found non-entitled MVPD required blackout')
|
|
3600
3624
|
blackouts[game_pk] = { blackout_type: '' }
|
|
3601
|
-
}
|
|
3625
|
+
}*/
|
|
3602
3626
|
|
|
3603
3627
|
// add blackout expiry, if requested
|
|
3604
|
-
if ( blackouts[game_pk] && calculate_expiries && await this.check_game_time(game.gameData) ) {
|
|
3628
|
+
if ( blackouts[game_pk] && (blackouts[game_pk].blackout_type != 'Not entitled') && calculate_expiries && await this.check_game_time(game.gameData) ) {
|
|
3605
3629
|
this.debuglog('get_blackout_games calculating blackout expiry')
|
|
3606
3630
|
let date_cache_data = await this.getDayData(gameDate)
|
|
3607
3631
|
if ( date_cache_data.dates && date_cache_data.dates[0] && date_cache_data.dates[0].games && (date_cache_data.dates[0].games.length > 0) ) {
|