├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── bin
└── cli.js
├── package.json
├── src
├── __tests__
│ └── cli.spec.js
├── cli.js
├── command
│ ├── Team.js
│ ├── __tests__
│ │ └── Team.spec.js
│ ├── game
│ │ ├── boxScore.js
│ │ ├── index.js
│ │ ├── live.js
│ │ ├── network.js
│ │ ├── preview.js
│ │ ├── schedule.js
│ │ └── scoreboard.js
│ ├── index.js
│ └── player
│ │ ├── index.js
│ │ ├── info.js
│ │ ├── infoCompare.js
│ │ ├── seasonStats.js
│ │ └── seasonStatsCompare.js
├── data
│ ├── boxscore.json
│ ├── playbyplay.json
│ └── scoreboard.json
└── utils
│ ├── __tests__
│ ├── blessed.spec.js
│ ├── catchAPIError.spec.js
│ ├── cfonts.spec.js
│ ├── convertUnit.spec.js
│ ├── getApiDate.spec.js
│ ├── log.spec.js
│ ├── nba.spec.js
│ └── table.spec.js
│ ├── blessed.js
│ ├── catchAPIError.js
│ ├── cfonts.js
│ ├── convertUnit.js
│ ├── fonts
│ ├── ter-u12b.json
│ └── ter-u12n.json
│ ├── getApiDate.js
│ ├── log.js
│ ├── nba.js
│ ├── setSeason.js
│ └── table.js
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": { "node": "6" }
7 | }
8 | ]
9 | ],
10 | "plugins": [
11 | "@babel/plugin-proposal-class-properties",
12 | "@babel/plugin-proposal-object-rest-spread",
13 | "@babel/plugin-transform-runtime"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | indent_size = 2
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules/**
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["eslint:recommended", "airbnb", "prettier"],
4 | "env": {
5 | "node": true,
6 | "jest": true
7 | },
8 | "rules": {
9 | "arrow-parens": "off",
10 | "consistent-return": "off",
11 | "camelcase": "off",
12 | "import/no-dynamic-require": "off",
13 | "global-require": "off",
14 | "no-underscore-dangle": "off",
15 | "no-console": "off",
16 | "import/prefer-default-export": "off",
17 | "prettier/prettier": [
18 | "error",
19 | {
20 | "trailingComma": "es5",
21 | "singleQuote": true
22 | }
23 | ]
24 | },
25 | "plugins": ["babel", "prettier"]
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # lib
61 | lib
62 |
63 | # pkg
64 | packed
65 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '10'
4 | - '8'
5 | - '6'
6 | cache:
7 | yarn: true
8 | directories:
9 | - 'node_modules'
10 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.6
2 | RUN apk --no-cache add nodejs-current nodejs-npm
3 | RUN npm set progress=false && npm install -g nba-go
4 | CMD ["nba-go", "game", "-t"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017-present Homer Chen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | > The finest NBA CLI.
13 |
14 | Watch NBA live play-by-play, game preview, box score and player information on your console.
15 | Best CLI tool for those who are both **NBA fans** and **Engineers**.
16 |
17 | All data comes from [stats.nba.com](http://stats.nba.com/) APIs.
18 |
19 | ## Install
20 |
21 | In order to use nba-go, make sure that you have [Node](https://nodejs.org/) version 6.0.0 or higher.
22 |
23 | ```
24 | $ npm install -g nba-go
25 | ```
26 |
27 | Or in a Docker Container:
28 |
29 | ```
30 | $ docker build -t nba-go:latest .
31 | $ docker run -it nba-go:latest
32 | ```
33 |
34 | By default, the docker container will run `nba-go game -t`, but you can
35 | override this command at run time.
36 | For example:
37 |
38 | ```
39 | $ docker run -it nba-go:latest nba-go player Curry -i
40 | ```
41 |
42 | Or download the latest version [pkg](https://github.com/zeit/pkg) binaries in [releases](https://github.com/xxhomey19/nba-go/releases). It can be run on Linux, macOs and Windows.
43 | For example:
44 |
45 | ```
46 | ./nba-go-macos game -h
47 | ```
48 |
49 | ## Usage
50 |
51 | `nba-go` provides two main commands.
52 |
53 | 1. [`game` or `g`](#game)
54 | 2. [`player` or `p`](#player)
55 |
56 | ### Game
57 |
58 | There are two things to do.
59 |
60 | 1. [**Check schedule**](#check-schedule).
61 | 2. Choose one game which you want to watch.
62 |
63 | Depending on the status of the game you chose, a different result will be shown. There are three kinds of statuses that may be displayed.
64 |
65 | | Status | Example | Description |
66 | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
67 | | [Pregame](#pregame) |
| It shows **when the game starts**.
Selecting this will show the comparison between two teams, including average points, field goal percents, average assists, etc. |
68 | | [Live](#live) |
| It shows **live game clock**.
**Most powerful feature!** Selecting this will show the live page which includes scoreboard, play-by-play and box score. |
69 | | [Final](#final) |
| Selecting this will show scoreboard, detailed box score, etc. |
70 |
71 | #### Check schedule
72 |
73 | In order to show the schedule on some days, `nba-go` provides the command `nba-go game` with some options.
74 |
75 | #### Options
76 |
77 | ##### `-d ` or `--date `
78 |
79 | Enter a specific date to check the schedule on that day.
80 |
81 | ```
82 | $ nba-go game -d 2017/11/02
83 | ```
84 |
85 | 
86 |
87 | ##### `-y` or `--yesterday`
88 |
89 | Check **yesterday's** schedule.
90 |
91 | ```
92 | $ nba-go game -y
93 | ```
94 |
95 | 
96 |
97 | ##### `-t` or `--today`
98 |
99 | Check **today's** schedule.
100 |
101 | ```
102 | $ nba-go game -t
103 | ```
104 |
105 | 
106 |
107 | ##### `-T` or `--tomorrow`
108 |
109 | Check **tomorrow's** schedule.
110 |
111 | ```
112 | $ nba-go game -T
113 | ```
114 |
115 | 
116 |
117 | ##### `-n` or `--networks`
118 |
119 | Display on schedule home team and away team television network information.
120 |
121 | ```
122 | $ nba-go game -n
123 | ```
124 |
125 | #### Pregame
126 |
127 | ⭐️⭐️
128 | Check the detailed comparison data between two teams in the game.
129 |
130 | 
131 |
132 | #### Live
133 |
134 | ⭐️⭐️⭐️
135 | **Best feature!** Realtime updated play-by-play, scoreboard and box score. Turn on fullscreen mode for better experience.
136 | Btw, play-by-play is scrollable!.
137 |
138 | 
139 |
140 | #### Final
141 |
142 | ⭐️⭐️
143 | Check two teams' detailed scoreboard and box score.
144 |
145 | 
146 |
147 | #### Filter
148 |
149 | Filter results to quickly jump to the info you care about
150 |
151 | #### Options
152 |
153 | ##### `-f` or `--filter`
154 |
155 | Currently only supports filtering the results by team but more options on the way
156 |
157 | ```
158 | nba-go game --filter team=Detroit
159 | ```
160 |
161 | ### Player
162 |
163 | Get player's basic information, regular season data and playoffs data.
164 |
165 | **Note.** Must place **player's name** between `nba-go player` and options.
166 |
167 | #### Options
168 |
169 | ##### `-i` or `--info`
170 |
171 | Get player's basic information.
172 |
173 | ```
174 | $ nba-go player Curry -i
175 | ```
176 |
177 | 
178 |
179 | ##### `-r` or `--regular`
180 |
181 | Get player's basic information.
182 |
183 | ```
184 | $ nba-go player Curry -r
185 | ```
186 |
187 | 
188 |
189 | ##### `-p` or `--playoffs`
190 |
191 | Get player's basic information.
192 |
193 | ```
194 | $ nba-go player Curry -p
195 | ```
196 |
197 | 
198 |
199 | ##### `-c` or `--compare`
200 |
201 | Get and compare the stats from multiple players. The better stat will be highlighted in green to make comparing easier.
202 | When listing the multiple names they must be in quotes and seperated by commas. Can be combined with the -i, -r, and -p flags.
203 |
204 | ```
205 | $ nba-go player "Lebron James, Stephen Curry, James Harden" -c -i -r -p
206 | ```
207 |
208 | 
209 |
210 | #### Mixed them all
211 |
212 | Get all data at the same time.
213 |
214 | ```
215 | $ nba-go player Curry -i -r -p
216 | ```
217 |
218 | 
219 |
220 | ## Development
221 |
222 | * It's simple to run `nba-go` on your local computer.
223 | * The following is step-by-step instruction.
224 |
225 | ```
226 | $ git clone https://github.com/xxhomey19/nba-go.git
227 | $ cd nba-go
228 | $ yarn
229 | $ NODE_ENV=development node bin/cli.js
230 | ```
231 |
232 | ## Related repo:
233 |
234 | - [nba-bar](https://github.com/xxhomey19/nba-bar)
235 | - [watch-nba](https://github.com/chentsulin/watch-nba)
236 | - [nba-color](https://github.com/xxhomey19/nba-color)
237 |
238 | ## License
239 |
240 | MIT © [xxhomey19](https://github.com/xxhomey19)
241 |
--------------------------------------------------------------------------------
/bin/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | if (process.env.NODE_ENV === 'development') {
4 | require('@babel/register');
5 |
6 | require('../src/cli');
7 | } else {
8 | require('../lib/cli');
9 | }
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nba-go",
3 | "description": "The finest NBA CLI.",
4 | "license": "MIT",
5 | "author": "xxhomey19",
6 | "homepage": "https://github.com/xxhomey19/nba-go#readme",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/xxhomey19/nba-go.git"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/xxhomey19/nba-go/issues"
13 | },
14 | "version": "0.4.0",
15 | "main": "lib/cli.js",
16 | "bin": {
17 | "nba-go": "lib/cli.js",
18 | "ng": "lib/cli.js"
19 | },
20 | "files": [
21 | "lib"
22 | ],
23 | "scripts": {
24 | "build": "npm run clean && webpack --config webpack.config.js -p",
25 | "clean": "rimraf lib packed",
26 | "dev": "webpack --config webpack.config.js -d -w",
27 | "lint": "eslint src",
28 | "lint:fix": "npm run lint -- --fix",
29 | "pack": "pkg . --out-path packed",
30 | "prepublishOnly": "npm run build && echo '#!/usr/bin/env node' | cat - lib/cli.js > temp && mv temp lib/cli.js",
31 | "test": "npm run lint:fix && npm run testonly:cov",
32 | "testonly": "NODE_ENV=test jest",
33 | "testonly:cov": "jest --coverage --runInBand --forceExit --no-cache",
34 | "testonly:watch": "jest --watch"
35 | },
36 | "dependencies": {
37 | "@babel/register": "^7.0.0",
38 | "async-to-gen": "^1.4.0",
39 | "blessed": "^0.1.81",
40 | "cfonts": "^2.4.0",
41 | "chalk": "^2.4.2",
42 | "cli-table3": "^0.5.1",
43 | "commander": "^2.19.0",
44 | "date-fns": "^1.30.1",
45 | "delay": "^4.1.0",
46 | "didyoumean": "^1.2.1",
47 | "inquirer": "^6.2.1",
48 | "is-async-supported": "^1.2.0",
49 | "log-update": "^2.2.0",
50 | "luxon": "^1.10.0",
51 | "nba": "^4.5.0",
52 | "nba-color": "^1.3.9",
53 | "nba-stats-client": "^1.0.0",
54 | "node-emoji": "^1.8.1",
55 | "ora": "^3.0.0",
56 | "p-map": "^2.0.0",
57 | "ramda": "^0.26.1",
58 | "stringz": "^1.0.0",
59 | "update-notifier": "^2.5.0",
60 | "wide-align": "^1.1.3"
61 | },
62 | "devDependencies": {
63 | "@babel/cli": "^7.2.3",
64 | "@babel/core": "^7.2.2",
65 | "@babel/plugin-proposal-class-properties": "^7.2.3",
66 | "@babel/plugin-proposal-object-rest-spread": "^7.2.0",
67 | "@babel/plugin-transform-runtime": "^7.2.0",
68 | "@babel/preset-env": "^7.2.3",
69 | "@babel/runtime": "^7.2.0",
70 | "babel-core": "^7.0.0-bridge.0",
71 | "babel-eslint": "^10.0.1",
72 | "babel-jest": "^23.6.0",
73 | "babel-loader": "^8.0.5",
74 | "copy-webpack-plugin": "^4.6.0",
75 | "eslint": "^5.12.0",
76 | "eslint-config-airbnb": "^17.1.0",
77 | "eslint-config-prettier": "^3.4.0",
78 | "eslint-plugin-babel": "^5.3.0",
79 | "eslint-plugin-import": "^2.14.0",
80 | "eslint-plugin-jsx-a11y": "^6.1.2",
81 | "eslint-plugin-prettier": "^3.0.1",
82 | "eslint-plugin-react": "^7.12.3",
83 | "husky": "^1.3.1",
84 | "jest": "^23.6.0",
85 | "lint-staged": "^8.1.0",
86 | "moment-timezone": "^0.5.23",
87 | "pkg": "^4.3.7",
88 | "prettier": "^1.15.3",
89 | "prettier-package-json": "^2.0.1",
90 | "rimraf": "^2.6.3",
91 | "terser-webpack-plugin": "^1.2.1",
92 | "webpack": "^4.28.4",
93 | "webpack-cli": "^3.2.1",
94 | "webpack-node-externals": "^1.7.2"
95 | },
96 | "keywords": [
97 | "NBA",
98 | "cli"
99 | ],
100 | "engines": {
101 | "node": ">=6.0.0"
102 | },
103 | "husky": {
104 | "hooks": {
105 | "pre-commit": "lint-staged"
106 | }
107 | },
108 | "jest": {
109 | "collectCoverageFrom": [
110 | "src/**/*.js"
111 | ],
112 | "coveragePathIgnorePatterns": [
113 | "/node_modules/",
114 | "/__tests__/"
115 | ],
116 | "testPathIgnorePatterns": [
117 | "node_modules/"
118 | ]
119 | },
120 | "lint-staged": {
121 | "package.json": [
122 | "prettier-package-json --write",
123 | "git add"
124 | ],
125 | "*.js": [
126 | "eslint --fix",
127 | "git add"
128 | ]
129 | },
130 | "pkg": {
131 | "scripts": [
132 | "lib/**/*.js",
133 | "node_modules/blessed/lib/**/*.js"
134 | ],
135 | "assets": "lib/data/fonts/*",
136 | "targets": [
137 | "node8-macos",
138 | "node8-linux",
139 | "node8-win"
140 | ]
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/__tests__/cli.spec.js:
--------------------------------------------------------------------------------
1 | jest.mock('update-notifier');
2 | jest.mock('chalk');
3 |
4 | jest.mock('../command');
5 | jest.mock('../utils/log');
6 |
7 | const _exit = process.exit;
8 |
9 | let log;
10 | let nbaGo;
11 | let updateNotifier;
12 |
13 | const setup = () => {
14 | updateNotifier = require('update-notifier');
15 | updateNotifier.mockReturnValue({ notify: jest.fn() });
16 |
17 | log = require('../utils/log');
18 | log.error = jest.fn(s => s);
19 | log.bold = jest.fn(s => s);
20 |
21 | nbaGo = require('../command');
22 |
23 | require('../cli');
24 | };
25 |
26 | describe('cli', () => {
27 | beforeEach(() => {
28 | process.exit = jest.fn();
29 | });
30 |
31 | afterEach(() => {
32 | process.exit = _exit;
33 | jest.resetModules();
34 | });
35 |
36 | it('should call updateNotifier at first', () => {
37 | process.argv = ['node', 'bin/cli.js', 'games'];
38 | setup();
39 |
40 | expect(updateNotifier).toBeCalled();
41 | });
42 |
43 | it('should call error when the command not matched', () => {
44 | process.argv = ['node', 'bin/cli.js', 'QQ'];
45 | setup();
46 |
47 | expect(log.error).toBeCalledWith('Unknown command: QQ');
48 | expect(process.exit).toBeCalledWith(1);
49 | });
50 |
51 | it('should call didYouMean when the command is similar to specific commands', () => {
52 | process.argv = ['node', 'bin/cli.js', 'games'];
53 | setup();
54 |
55 | expect(log.error.mock.calls.length).toBe(2);
56 | expect(log.error.mock.calls[0][0]).toBe(
57 | `Unknown command: ${log.bold('games')}`
58 | );
59 | expect(log.error.mock.calls[1][0]).toBe(
60 | `Did you mean ${log.bold('game')} ?`
61 | );
62 | expect(process.exit).toBeCalledWith(1);
63 | });
64 |
65 | describe('player command', () => {
66 | it('should call nbaGo with option -i', () => {
67 | process.argv = ['node', 'bin/cli.js', 'player', 'Curry', '-i'];
68 | setup();
69 |
70 | expect(nbaGo.player.mock.calls[0][0]).toBe('Curry');
71 | expect(nbaGo.player.mock.calls[0][1].info).toBe(true);
72 | });
73 |
74 | it('should call nbaGo with option -r', () => {
75 | process.argv = ['node', 'bin/cli.js', 'player', 'Curry', '-r'];
76 | setup();
77 |
78 | expect(nbaGo.player.mock.calls[0][0]).toBe('Curry');
79 | expect(nbaGo.player.mock.calls[0][1].regular).toBe(true);
80 | });
81 |
82 | it('should call nbaGo with option -p', () => {
83 | process.argv = ['node', 'bin/cli.js', 'player', 'Curry', '-p'];
84 | setup();
85 |
86 | expect(nbaGo.player.mock.calls[0][0]).toBe('Curry');
87 | expect(nbaGo.player.mock.calls[0][1].playoffs).toBe(true);
88 | });
89 |
90 | it('should call nbaGo with option -i when user did not enter any option', () => {
91 | process.argv = ['node', 'bin/cli.js', 'player', 'Curry'];
92 | setup();
93 |
94 | expect(nbaGo.player.mock.calls[0][0]).toBe('Curry');
95 | expect(nbaGo.player.mock.calls[0][1].info).toBe(true);
96 | });
97 |
98 | it('alias should work', () => {
99 | process.argv = ['node', 'bin/cli.js', 'p', 'Curry'];
100 | setup();
101 |
102 | expect(nbaGo.player.mock.calls[0][0]).toBe('Curry');
103 | expect(nbaGo.player.mock.calls[0][1].info).toBe(true);
104 | });
105 | });
106 |
107 | describe('game command', () => {
108 | it('should call nbaGo with option -t when user did not enter any option', () => {
109 | process.argv = ['node', 'bin/cli.js', 'game'];
110 | setup();
111 |
112 | expect(nbaGo.game.mock.calls[0][0].today).toBe(true);
113 | });
114 |
115 | it('should call nbaGo with option -d and date', () => {
116 | process.argv = ['node', 'bin/cli.js', 'game', '-d', '2017/11/11'];
117 | setup();
118 |
119 | expect(nbaGo.game.mock.calls[0][0].date).toBe('2017/11/11');
120 | });
121 |
122 | it('should call nbaGo with option -y', () => {
123 | process.argv = ['node', 'bin/cli.js', 'game', '-y'];
124 | setup();
125 |
126 | expect(nbaGo.game.mock.calls[0][0].yesterday).toBe(true);
127 | });
128 |
129 | it('should call nbaGo with option -t', () => {
130 | process.argv = ['node', 'bin/cli.js', 'game', '-t'];
131 | setup();
132 |
133 | expect(nbaGo.game.mock.calls[0][0].today).toBe(true);
134 | });
135 |
136 | it('should call nbaGo with option -T', () => {
137 | process.argv = ['node', 'bin/cli.js', 'game', '-T'];
138 | setup();
139 |
140 | expect(nbaGo.game.mock.calls[0][0].tomorrow).toBe(true);
141 | });
142 |
143 | it('should call nbaGo with option -n', () => {
144 | process.argv = ['node', 'bin/cli.js', 'game', '-n'];
145 | setup();
146 |
147 | expect(nbaGo.game.mock.calls[0][0].networks).toBe(true);
148 | });
149 |
150 | it('alias should work', () => {
151 | process.argv = ['node', 'bin/cli.js', 'p', 'Curry'];
152 | setup();
153 |
154 | expect(nbaGo.player.mock.calls[0][0]).toBe('Curry');
155 | expect(nbaGo.player.mock.calls[0][1].info).toBe(true);
156 | });
157 |
158 | it('should call nbaGo with option to view specific team', () => {
159 | process.argv = ['node', 'bin/cli.js', 'game', '--filter', 'team=Pistons'];
160 | setup();
161 |
162 | expect(nbaGo.game.mock.calls[0][0].filter).toBe('team=Pistons');
163 | });
164 | });
165 | });
166 |
--------------------------------------------------------------------------------
/src/cli.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 |
3 | import program from 'commander';
4 | import didYouMean from 'didyoumean';
5 | import isAsyncSupported from 'is-async-supported';
6 | import chalk from 'chalk';
7 | import updateNotifier from 'update-notifier';
8 |
9 | import { player as playerCommand, game as gameCommand } from './command';
10 | import { error, bold, nbaRed, neonGreen } from './utils/log';
11 |
12 | import pkg from '../package.json';
13 |
14 | if (!isAsyncSupported()) {
15 | require('async-to-gen/register');
16 | }
17 |
18 | (async () => {
19 | await updateNotifier({
20 | pkg,
21 | }).notify({ defer: false });
22 | })();
23 |
24 | program.version(
25 | `\n${chalk`{bold.hex('#0069b9') NBA}`} ${nbaRed('GO')} version: ${
26 | pkg.version
27 | }\n`,
28 | '-v, --version'
29 | );
30 |
31 | program
32 | .command('player ')
33 | .alias('p')
34 | .option('-i, --info', "Check the player's basic information")
35 | .option('-r, --regular', "Check the player's career regular season data")
36 | .option('-p, --playoffs', "Check the player's career playoffs data")
37 | .option(
38 | '-c, --compare',
39 | "Compare the stats of two or more players. Seperate the names with commas, ex:'Stepen Curry, Lebron James'"
40 | )
41 | .on('--help', () => {
42 | console.log('');
43 | console.log(
44 | " Get player's basic information, regular season data and playoffs data."
45 | );
46 | console.log('');
47 | console.log(' Example:');
48 | console.log(
49 | ` ${neonGreen(
50 | 'nba-go player Curry'
51 | )} => Show both Seth Curry's and Stephen Curry's basic information.`
52 | );
53 | console.log(
54 | ` ${neonGreen(
55 | 'nba-go player Curry -r'
56 | )} => Show both Seth Curry's and Stephen Curry's regular season data.`
57 | );
58 | console.log('');
59 | console.log(` For more detailed information, please check the GitHub page: ${neonGreen(
60 | 'https://github.com/xxhomey19/nba-go#player'
61 | )}
62 | `);
63 | })
64 | .action((name, option) => {
65 | if (!option.info && !option.regular && !option.playoffs) {
66 | option.info = true;
67 | }
68 |
69 | playerCommand(name, option);
70 | });
71 |
72 | program
73 | .command('game')
74 | .alias('g')
75 | .option('-d, --date ', 'Watch games at specific date')
76 | .option('-y, --yesterday', "Watch yesterday's games")
77 | .option('-t, --today', "Watch today's games")
78 | .option('-T, --tomorrow', "Watch tomorrow's games")
79 | .option('-f, --filter ', 'Filter game choices to watch')
80 | .option('-n, --networks', 'See the networks game is/was televised on.')
81 | .on('--help', () => {
82 | console.log('');
83 | console.log(' Watch NBA live play-by-play, game preview and box score.');
84 | console.log(" You have to enter what day's schedule at first.");
85 | console.log(
86 | ` Notice that if you don't provide any option, default date will be ${neonGreen(
87 | 'today'
88 | )}.`
89 | );
90 | console.log('');
91 | console.log(' Example:');
92 | console.log(
93 | ` ${neonGreen(
94 | 'nba-go game -d 2017/11/11'
95 | )} => Show game schedule on 2017/11/11.`
96 | );
97 | console.log(
98 | ` ${neonGreen(
99 | 'nba-go game -t'
100 | )} => Show today's game schedule.`
101 | );
102 | console.log('');
103 | console.log(` For more detailed information, please check the GitHub page: ${neonGreen(
104 | 'https://github.com/xxhomey19/nba-go#game'
105 | )}
106 | `);
107 | })
108 | .action(option => {
109 | if (
110 | !option.date &&
111 | !option.yesterday &&
112 | !option.today &&
113 | !option.tomorrow
114 | ) {
115 | option.today = true;
116 | }
117 |
118 | gameCommand(option);
119 | });
120 |
121 | program.on('--help', () => {
122 | console.log('');
123 | console.log('');
124 | console.log(
125 | ` Welcome to ${chalk`{bold.hex('#0069b9') NBA}`} ${nbaRed('GO')} !`
126 | );
127 | console.log('');
128 | console.log(
129 | ` Wanna watch NBA game please enter: ${neonGreen('nba-go game')}`
130 | );
131 | console.log(
132 | ` Wanna check NBA player information please enter: ${neonGreen(
133 | 'nba-go player '
134 | )}`
135 | );
136 | console.log('');
137 | console.log(
138 | ` For more detailed information please check the GitHub page: ${neonGreen(
139 | 'https://github.com/xxhomey19/nba-go'
140 | )}`
141 | );
142 | console.log(
143 | ` Or enter ${neonGreen('nba-go game -h')}, ${neonGreen(
144 | 'nba-go player -h'
145 | )} to get more helpful information.`
146 | );
147 | console.log('');
148 | });
149 |
150 | program.command('*').action(command => {
151 | error(`Unknown command: ${bold(command)}`);
152 |
153 | const commandNames = program.commands
154 | .map(c => c._name)
155 | .filter(name => name !== '*');
156 |
157 | const closeMatch = didYouMean(command, commandNames);
158 |
159 | if (closeMatch) {
160 | error(`Did you mean ${bold(closeMatch)} ?`);
161 | }
162 |
163 | process.exit(1);
164 | });
165 |
166 | if (process.argv.length === 2) program.help();
167 |
168 | program.parse(process.argv);
169 |
--------------------------------------------------------------------------------
/src/command/Team.js:
--------------------------------------------------------------------------------
1 | import emoji from 'node-emoji';
2 | import { getMainColor } from 'nba-color';
3 |
4 | import { colorTeamName } from '../utils/log';
5 |
6 | export default class Team {
7 | constructor({
8 | teamId,
9 | teamCity,
10 | teamName,
11 | teamAbbreviation,
12 | score,
13 | w,
14 | l,
15 | divRank,
16 | linescores,
17 | isHomeTeam,
18 | }) {
19 | this.id = teamId;
20 | this.city = teamCity;
21 | this.name = teamName;
22 | this.abbreviation = teamAbbreviation;
23 | this.score = score === '' ? '0' : score;
24 | this.wins = w;
25 | this.loses = l;
26 | this.divRank = divRank;
27 | this.gameStats = {};
28 | this.players = [];
29 | this.gameLeaders = {};
30 | this.color = getMainColor(teamAbbreviation);
31 | this.isHomeTeam = isHomeTeam;
32 |
33 | if (linescores) {
34 | this.linescores = Array.isArray(linescores.period)
35 | ? linescores.period
36 | : [linescores.period];
37 | } else {
38 | this.linescores = [];
39 | }
40 | }
41 |
42 | getID() {
43 | return this.id;
44 | }
45 |
46 | getCity() {
47 | return this.city;
48 | }
49 |
50 | getAbbreviation({ color }) {
51 | return color === false
52 | ? this.abbreviation
53 | : colorTeamName(this.getColor(), this.abbreviation);
54 | }
55 |
56 | getName({ color }) {
57 | return color === false
58 | ? this.name
59 | : colorTeamName(this.getColor(), this.name);
60 | }
61 |
62 | getScore() {
63 | return this.score;
64 | }
65 |
66 | getWins() {
67 | return this.wins;
68 | }
69 |
70 | getLoses() {
71 | return this.loses;
72 | }
73 |
74 | getFullName({ color }) {
75 | return color === false
76 | ? `${this.city} ${this.name}`
77 | : colorTeamName(this.getColor(), `${this.city} ${this.name}`);
78 | }
79 |
80 | getColor() {
81 | return this.color ? this.color.hex : undefined;
82 | }
83 |
84 | getWinnerName(direction) {
85 | return direction === 'left'
86 | ? `${emoji.get('crown')} ${colorTeamName(this.getColor(), this.name)}`
87 | : `${colorTeamName(this.getColor(), this.name)} ${emoji.get('crown')}`;
88 | }
89 |
90 | getQuarterScore(quarter) {
91 | return this.linescores.find(
92 | quarterData => quarterData.period_value === quarter
93 | ).score;
94 | }
95 |
96 | getGameStats() {
97 | return this.gameStats;
98 | }
99 |
100 | getPlayers() {
101 | return this.players;
102 | }
103 |
104 | getGameLeaders(sector) {
105 | return (
106 | this.gameLeaders[sector] || {
107 | StatValue: '-',
108 | leader: [
109 | {
110 | FirstName: '',
111 | LastName: '',
112 | },
113 | ],
114 | }
115 | );
116 | }
117 |
118 | getIsHomeTeam() {
119 | return this.isHomeTeam;
120 | }
121 |
122 | setScore(score) {
123 | this.score = score;
124 | }
125 |
126 | setGameStats(stats) {
127 | this.gameStats = stats;
128 | }
129 |
130 | setPlayers(players) {
131 | this.players = players;
132 | }
133 |
134 | setGameLeaders(leaders) {
135 | this.gameLeaders = leaders;
136 | }
137 |
138 | setQuarterScore(quarter, score) {
139 | this.linescores.find(
140 | quarterData => quarterData.period_value === quarter
141 | ).score = score;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/command/__tests__/Team.spec.js:
--------------------------------------------------------------------------------
1 | import Team from '../Team';
2 |
3 | jest.mock('node-emoji');
4 | jest.mock('../../utils/log');
5 |
6 | const emoji = require('node-emoji');
7 | const { colorTeamName } = require('../../utils/log');
8 |
9 | const setup = () =>
10 | new Team({
11 | teamId: '123',
12 | teamCity: 'LA',
13 | teamName: 'Lakers',
14 | teamAbbreviation: 'LAL',
15 | score: '100',
16 | w: '1',
17 | l: '1',
18 | divRank: '1',
19 | linescores: {
20 | period: [{ period_value: '1', period_name: 'Q1', score: '1' }],
21 | },
22 | isHomeTeam: true,
23 | });
24 |
25 | describe('Team', () => {
26 | afterEach(() => {
27 | jest.resetAllMocks();
28 | });
29 |
30 | it('score should be 0 when parameter score is empty string', () => {
31 | const team = new Team({
32 | teamAbbreviation: 'LAL',
33 | score: '',
34 | });
35 |
36 | const score = team.getScore();
37 |
38 | expect(score).toBe('0');
39 | });
40 |
41 | it('linescores should be array when linescores score is an object', () => {
42 | const team = new Team({
43 | teamAbbreviation: 'LAL',
44 | linescores: {
45 | period: { period_value: '1', period_name: 'Q1', score: '1' },
46 | },
47 | });
48 |
49 | const score = team.getQuarterScore('1');
50 |
51 | expect(score).toBe('1');
52 | });
53 |
54 | it('method getID should work', () => {
55 | const team = setup();
56 | const id = team.getID();
57 |
58 | expect(id).toBe('123');
59 | });
60 |
61 | it('method getCity should work', () => {
62 | const team = setup();
63 | const city = team.getCity();
64 |
65 | expect(city).toBe('LA');
66 | });
67 |
68 | it('method getAbbreviation and color is false should work', () => {
69 | const team = setup();
70 | const abbreviation = team.getAbbreviation({ color: false });
71 |
72 | expect(abbreviation).toBe('LAL');
73 | expect(colorTeamName).not.toBeCalled();
74 | });
75 |
76 | it('method getAbbreviation and color is true should work', () => {
77 | colorTeamName.mockReturnValueOnce('LAL');
78 | const team = setup();
79 | const abbreviation = team.getAbbreviation({ color: true });
80 |
81 | expect(abbreviation).toBe('LAL');
82 | expect(colorTeamName).toBeCalled();
83 | });
84 |
85 | it('method getName and color is false should work', () => {
86 | const team = setup();
87 | const name = team.getName({ color: false });
88 |
89 | expect(name).toBe('Lakers');
90 | expect(colorTeamName).not.toBeCalled();
91 | });
92 |
93 | it('method getName and color is true should work', () => {
94 | colorTeamName.mockReturnValueOnce('Lakers');
95 | const team = setup();
96 | const name = team.getName({ color: true });
97 |
98 | expect(name).toBe('Lakers');
99 | expect(colorTeamName).toBeCalled();
100 | });
101 |
102 | it('method setScore and getScore should work', () => {
103 | const team = setup();
104 | const score = team.getScore();
105 |
106 | expect(score).toBe('100');
107 |
108 | team.setScore('102');
109 | const newScore = team.getScore();
110 |
111 | expect(newScore).toBe('102');
112 | });
113 |
114 | it('method getWins should work', () => {
115 | const team = setup();
116 | const wins = team.getWins();
117 |
118 | expect(wins).toBe('1');
119 | });
120 |
121 | it('method getLoses should work', () => {
122 | const team = setup();
123 | const loses = team.getLoses();
124 |
125 | expect(loses).toBe('1');
126 | });
127 |
128 | it('method getFullName and color is false should work', () => {
129 | const team = setup();
130 | const fullName = team.getFullName({ color: false });
131 |
132 | expect(fullName).toBe('LA Lakers');
133 | expect(colorTeamName).not.toBeCalled();
134 | });
135 |
136 | it('method getFullName and color is true should work', () => {
137 | colorTeamName.mockReturnValueOnce('LA Lakers');
138 | const team = setup();
139 | const fullName = team.getFullName({ color: true });
140 |
141 | expect(fullName).toBe('LA Lakers');
142 | expect(colorTeamName).toBeCalled();
143 | });
144 |
145 | it('method getColor should work', () => {
146 | const team = setup();
147 | const color = team.getColor();
148 |
149 | expect(color).toBe('#702f8a');
150 | });
151 |
152 | it('method getColor should return undefined when passing unknown teamAbbreviation', () => {
153 | const team = new Team({
154 | teamAbbreviation: 'QQQQ',
155 | });
156 | const color = team.getColor();
157 |
158 | expect(color).toBe(undefined);
159 | });
160 |
161 | it('method getWinnerName and direction is left should work', () => {
162 | colorTeamName.mockReturnValueOnce('LA Lakers');
163 | emoji.get.mockReturnValueOnce('Crown');
164 | const team = setup();
165 | const winnerName = team.getWinnerName('left');
166 |
167 | expect(winnerName).toBe('Crown LA Lakers');
168 | expect(colorTeamName).toBeCalled();
169 | });
170 |
171 | it('method getWinnerName and direction is right should work', () => {
172 | colorTeamName.mockReturnValueOnce('LA Lakers');
173 | emoji.get.mockReturnValueOnce('Crown');
174 | const team = setup();
175 | const winnerName = team.getWinnerName('right');
176 |
177 | expect(winnerName).toBe('LA Lakers Crown');
178 | expect(colorTeamName).toBeCalled();
179 | });
180 |
181 | it('method setQuarterScore and getQuarterScore should work', () => {
182 | const team = setup();
183 | const score = team.getQuarterScore('1');
184 |
185 | expect(score).toBe('1');
186 |
187 | team.setQuarterScore('1', '3');
188 | const newScore = team.getQuarterScore('1');
189 |
190 | expect(newScore).toBe('3');
191 | });
192 |
193 | it('method getGameStats and setGameStats should work', () => {
194 | const team = setup();
195 | const stats = {
196 | points: '97',
197 | field_goals_made: '40',
198 | field_goals_attempted: '85',
199 | field_goals_percentage: '47.1',
200 | free_throws_made: '15',
201 | free_throws_attempted: '16',
202 | free_throws_percentage: '93.8',
203 | three_pointers_made: '2',
204 | three_pointers_attempted: '12',
205 | three_pointers_percentage: '16.7',
206 | rebounds_offensive: '9',
207 | rebounds_defensive: '25',
208 | team_rebounds: '7',
209 | assists: '12',
210 | fouls: '21',
211 | team_fouls: '6',
212 | technical_fouls: '1',
213 | steals: '11',
214 | turnovers: '12',
215 | team_turnovers: '2',
216 | blocks: '2',
217 | short_timeout_remaining: '1',
218 | full_timeout_remaining: '1',
219 | };
220 |
221 | team.setGameStats(stats);
222 | const gameStats = team.getGameStats();
223 |
224 | expect(gameStats).toEqual(stats);
225 | });
226 |
227 | it('method getPlayers and setPlayers should work', () => {
228 | const team = setup();
229 | const players = [
230 | {
231 | first_name: 'Kawhi',
232 | last_name: 'Leonard',
233 | jersey_number: '2',
234 | person_id: '202695',
235 | position_short: 'SF',
236 | position_full: 'Forward',
237 | minutes: '40',
238 | seconds: '18',
239 | points: '21',
240 | },
241 | {
242 | first_name: 'LaMarcus',
243 | last_name: 'Aldridge',
244 | jersey_number: '12',
245 | person_id: '200746',
246 | position_short: 'PF',
247 | position_full: 'Forward',
248 | minutes: '37',
249 | seconds: '6',
250 | points: '20',
251 | },
252 | ];
253 |
254 | team.setPlayers(players);
255 | const gamePlayers = team.getPlayers();
256 |
257 | expect(gamePlayers).toEqual(players);
258 | });
259 |
260 | it('method getGameLeaders and setGameLeaders should work', () => {
261 | const team = setup();
262 | const leaders = {
263 | Points: {
264 | PlayerCount: '1',
265 | StatValue: '22',
266 | leader: [
267 | {
268 | PersonID: '2225',
269 | PlayerCode: 'tony_parker',
270 | FirstName: 'Tony',
271 | LastName: 'Parker',
272 | },
273 | ],
274 | },
275 | };
276 |
277 | team.setGameLeaders(leaders);
278 | const gameLeaders = team.getGameLeaders('Points');
279 |
280 | expect(gameLeaders).toEqual(leaders.Points);
281 | });
282 |
283 | it('method getGameLeaders should work even did not set at first', () => {
284 | const team = setup();
285 | const gameLeaders = team.getGameLeaders('Points');
286 |
287 | expect(gameLeaders).toEqual({
288 | StatValue: '-',
289 | leader: [
290 | {
291 | FirstName: '',
292 | LastName: '',
293 | },
294 | ],
295 | });
296 | });
297 |
298 | it('method getIsHomeTeam should work', () => {
299 | const team = setup();
300 | const isHomeTeam = team.getIsHomeTeam();
301 |
302 | expect(isHomeTeam).toBe(true);
303 | });
304 | });
305 |
--------------------------------------------------------------------------------
/src/command/game/boxScore.js:
--------------------------------------------------------------------------------
1 | import { basicTable } from '../../utils/table';
2 | import { bold, neonGreen, nbaRed } from '../../utils/log';
3 |
4 | const alignCenter = columns =>
5 | columns.map(content => ({ content, hAlign: 'left', vAlign: 'center' }));
6 |
7 | const checkOverStandard = (record, standard) =>
8 | +record >= standard ? nbaRed(record) : record;
9 |
10 | const checkGameHigh = (players, record, recordVal, standard) => {
11 | const recordArr = players.map(player => Number.parseInt(player[record], 10));
12 | return recordVal >= Math.max(...recordArr)
13 | ? neonGreen(recordVal)
14 | : checkOverStandard(recordVal, standard);
15 | };
16 |
17 | const createTeamBoxScore = team => {
18 | const players = team.getPlayers();
19 | const stats = team.getGameStats();
20 | const boxScoreTable = basicTable();
21 |
22 | boxScoreTable.push(
23 | [
24 | {
25 | colSpan: 16,
26 | content: team.getFullName({ color: true }),
27 | hAlign: 'left',
28 | vAlign: 'center',
29 | },
30 | ],
31 | alignCenter([
32 | bold('PLAYER'),
33 | bold('POS'),
34 | bold('MIN'),
35 | bold('FG'),
36 | bold('3FG'),
37 | bold('FT'),
38 | bold('+/-'),
39 | bold('OREB'),
40 | bold('DREB'),
41 | bold('REB'),
42 | bold('AST'),
43 | bold('STL'),
44 | bold('BLK'),
45 | bold('TO'),
46 | bold('PF'),
47 | bold('PTS'),
48 | ])
49 | );
50 |
51 | players.forEach(player => {
52 | const {
53 | first_name,
54 | last_name,
55 | position_short,
56 | minutes,
57 | field_goals_made,
58 | field_goals_attempted,
59 | three_pointers_made,
60 | three_pointers_attempted,
61 | free_throws_made,
62 | free_throws_attempted,
63 | plus_minus,
64 | rebounds_offensive,
65 | rebounds_defensive,
66 | assists,
67 | steals,
68 | blocks,
69 | turnovers,
70 | fouls,
71 | points,
72 | } = player;
73 |
74 | const totalRebounds = +rebounds_offensive + +rebounds_defensive;
75 |
76 | boxScoreTable.push(
77 | alignCenter([
78 | bold(`${first_name} ${last_name}`),
79 | bold(position_short),
80 | checkGameHigh(players, 'minutes', minutes, 35),
81 | `${field_goals_made}-${field_goals_attempted}`,
82 | `${three_pointers_made}-${three_pointers_attempted}`,
83 | `${free_throws_made}-${free_throws_attempted}`,
84 | checkGameHigh(players, 'plus_minus', plus_minus, 15),
85 | checkGameHigh(players, 'rebounds_offensive', rebounds_offensive, 10),
86 | checkGameHigh(players, 'rebounds_defensive', rebounds_defensive, 10),
87 | checkGameHigh(players, 'totalRebounds', totalRebounds, 10),
88 | checkGameHigh(players, 'assists', assists, 10),
89 | checkGameHigh(players, 'steals', steals, 5),
90 | checkGameHigh(players, 'blocks', blocks, 5),
91 | checkGameHigh(players, 'turnovers', turnovers, 5),
92 | checkGameHigh(players, 'fouls', fouls, 6),
93 | checkGameHigh(players, 'points', points, 20),
94 | ])
95 | );
96 | });
97 |
98 | const {
99 | points,
100 | field_goals_made,
101 | field_goals_attempted,
102 | free_throws_made,
103 | free_throws_attempted,
104 | three_pointers_made,
105 | three_pointers_attempted,
106 | rebounds_offensive,
107 | rebounds_defensive,
108 | assists,
109 | fouls,
110 | steals,
111 | turnovers,
112 | blocks,
113 | } = stats;
114 |
115 | boxScoreTable.push(
116 | alignCenter([
117 | 'Totals',
118 | '',
119 | '',
120 | bold(`${field_goals_made}-${field_goals_attempted}`),
121 | bold(`${three_pointers_made}-${three_pointers_attempted}`),
122 | bold(`${free_throws_made}-${free_throws_attempted}`),
123 | '',
124 | bold(rebounds_offensive),
125 | bold(rebounds_defensive),
126 | bold(parseInt(rebounds_offensive, 10) + parseInt(rebounds_defensive, 10)),
127 | bold(assists),
128 | bold(steals),
129 | bold(blocks),
130 | bold(turnovers),
131 | bold(fouls),
132 | bold(neonGreen(points)),
133 | ])
134 | );
135 |
136 | console.log(boxScoreTable.toString());
137 | };
138 |
139 | const boxScore = (homeTeam, visitorTeam) => {
140 | createTeamBoxScore(homeTeam);
141 | console.log('');
142 | createTeamBoxScore(visitorTeam);
143 | };
144 |
145 | export default boxScore;
146 |
--------------------------------------------------------------------------------
/src/command/game/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-await-in-loop, no-constant-condition */
2 |
3 | import R from 'ramda';
4 | import parse from 'date-fns/parse';
5 | import addDays from 'date-fns/add_days';
6 | import subDays from 'date-fns/sub_days';
7 | import format from 'date-fns/format';
8 | import isValid from 'date-fns/is_valid';
9 | import emoji from 'node-emoji';
10 | import delay from 'delay';
11 | import ora from 'ora';
12 |
13 | import chooseGameFromSchedule, { getTeamInfo } from './schedule';
14 | import preview from './preview';
15 | import scoreboard from './scoreboard';
16 | import boxScore from './boxScore';
17 | import live from './live';
18 | import getBroadcastNetworks from './network';
19 |
20 | import setSeason from '../../utils/setSeason';
21 | import getApiDate from '../../utils/getApiDate';
22 | import NBA from '../../utils/nba';
23 | import { error, bold } from '../../utils/log';
24 | import { cfontsDate } from '../../utils/cfonts';
25 | import getBlessed from '../../utils/blessed';
26 | import catchAPIError from '../../utils/catchAPIError';
27 |
28 | const getGameWithOptionalFilter = async (games, option) => {
29 | if (option.filter && option.filter.split('=')[0] === 'team') {
30 | // TODO: Add more robust filtering but use team as proof of concept
31 | const components = option.filter.split('=');
32 | const team = components[1].toLowerCase();
33 | const potentialGames = games.filter(
34 | data =>
35 | `${data.home.city.toLowerCase()} ${data.home.nickname.toLowerCase()}`.indexOf(
36 | team
37 | ) !== -1 ||
38 | `${data.visitor.city.toLowerCase()} ${data.visitor.nickname.toLowerCase()}`.indexOf(
39 | team
40 | ) !== -1
41 | );
42 |
43 | if (!potentialGames.length) {
44 | error(`Can't find any teams that match ${team}`);
45 | } else if (potentialGames.length === 1) {
46 | const homeTeam = await getTeamInfo(potentialGames[0].home);
47 | const visitorTeam = await getTeamInfo(potentialGames[0].visitor);
48 |
49 | return { game: { gameData: potentialGames[0], homeTeam, visitorTeam } };
50 | } else {
51 | return chooseGameFromSchedule(potentialGames);
52 | }
53 | }
54 |
55 | return chooseGameFromSchedule(games, option);
56 | };
57 |
58 | const game = async option => {
59 | let _date;
60 | let gamesData;
61 | let gameBoxScoreData;
62 | let seasonMetaData;
63 |
64 | if (option.date) {
65 | if (
66 | R.compose(
67 | isValid,
68 | parse
69 | )(option.date)
70 | ) {
71 | _date = format(option.date, 'YYYY-MM-DD');
72 | } else {
73 | error('Date is invalid');
74 | process.exit(1);
75 | }
76 | } else if (option.today) {
77 | _date = Date.now();
78 | } else if (option.tomorrow) {
79 | _date = addDays(Date.now(), 1);
80 | } else if (option.yesterday) {
81 | _date = subDays(Date.now(), 1);
82 | } else {
83 | error(`Can't find any option ${emoji.get('confused')}`);
84 | process.exit(1);
85 | }
86 |
87 | R.compose(
88 | cfontsDate,
89 | setSeason
90 | )(_date);
91 |
92 | const apiDate = getApiDate(_date);
93 |
94 | try {
95 | const {
96 | sports_content: {
97 | games: { game: _gamesData },
98 | },
99 | } = await NBA.getGames(apiDate);
100 |
101 | gamesData = _gamesData;
102 | } catch (err) {
103 | catchAPIError(err, 'NBA.getGames()');
104 | }
105 |
106 | const {
107 | game: { homeTeam, visitorTeam, gameData },
108 | } = await getGameWithOptionalFilter(gamesData, option);
109 |
110 | try {
111 | const {
112 | sports_content: {
113 | game: _gameBoxScoreData,
114 | sports_meta: { season_meta: _seasonMetaData },
115 | },
116 | } = await NBA.getBoxScore({ ...apiDate, gameId: gameData.id });
117 |
118 | gameBoxScoreData = _gameBoxScoreData;
119 | seasonMetaData = _seasonMetaData;
120 | } catch (err) {
121 | catchAPIError(err, 'NBA.getBoxScore()');
122 | }
123 |
124 | const { home, visitor } = gameBoxScoreData;
125 |
126 | homeTeam.setGameStats(home.stats);
127 | homeTeam.setPlayers(home.players.player);
128 | homeTeam.setGameLeaders(home.Leaders);
129 | visitorTeam.setGameStats(visitor.stats);
130 | visitorTeam.setPlayers(visitor.players.player);
131 | visitorTeam.setGameLeaders(visitor.Leaders);
132 |
133 | const {
134 | screen,
135 | scoreboardTable,
136 | seasonText,
137 | timeText,
138 | dateText,
139 | arenaText,
140 | networkText,
141 | homeTeamScoreText,
142 | visitorTeamScoreText,
143 | playByPlayBox,
144 | boxscoreTable,
145 | } = getBlessed(homeTeam, visitorTeam);
146 |
147 | switch (gameData.period_time.game_status) {
148 | case '1': {
149 | screen.destroy();
150 | console.log('');
151 |
152 | const spinner = ora('Loading Game Preview').start();
153 |
154 | let homeTeamDashboardData;
155 | let visitorTeamDashboardData;
156 |
157 | try {
158 | const {
159 | overallTeamDashboard: [_homeTeamDashboardData],
160 | } = await NBA.teamSplits({
161 | Season: process.env.season,
162 | TeamID: homeTeam.getID(),
163 | });
164 | const {
165 | overallTeamDashboard: [_visitorTeamDashboardData],
166 | } = await NBA.teamSplits({
167 | Season: process.env.season,
168 | TeamID: visitorTeam.getID(),
169 | });
170 |
171 | homeTeamDashboardData = _homeTeamDashboardData;
172 | visitorTeamDashboardData = _visitorTeamDashboardData;
173 | } catch (err) {
174 | catchAPIError(err, 'NBA.teamSplits()');
175 | }
176 |
177 | spinner.stop();
178 |
179 | preview(homeTeam, visitorTeam, {
180 | ...seasonMetaData,
181 | ...gameBoxScoreData,
182 | homeTeamDashboardData,
183 | visitorTeamDashboardData,
184 | });
185 | break;
186 | }
187 |
188 | case 'Halftime':
189 | case '2': {
190 | let updatedPlayByPlayData;
191 | let updatedGameBoxScoreData;
192 |
193 | seasonText.setContent(
194 | bold(`${seasonMetaData.display_year} ${seasonMetaData.display_season}`)
195 | );
196 | const { arena, city, state, date, time, broadcasters } = gameBoxScoreData;
197 |
198 | const networks = getBroadcastNetworks(broadcasters.tv.broadcaster);
199 |
200 | dateText.setContent(
201 | `${emoji.get('calendar')} ${format(date, 'YYYY/MM/DD')} ${time.slice(
202 | 0,
203 | 2
204 | )}:${time.slice(2, 4)}`
205 | );
206 | arenaText.setContent(
207 | `${emoji.get('house')} ${arena} | ${city}, ${state}`
208 | );
209 | networkText.setContent(
210 | `${networks.homeTeam} ${emoji.get('tv')} ${networks.visitorTeam}`
211 | );
212 | while (true) {
213 | let gamePlayByPlayData = {};
214 |
215 | try {
216 | const {
217 | sports_content: { game: _updatedPlayByPlayData },
218 | } = await NBA.getPlayByPlay({ ...apiDate, gameId: gameData.id });
219 |
220 | updatedPlayByPlayData = _updatedPlayByPlayData;
221 | } catch (err) {
222 | catchAPIError(err, 'NBA.getPlayByPlay()');
223 | }
224 |
225 | try {
226 | const {
227 | sports_content: { game: _updatedGameBoxScoreData },
228 | } = await NBA.getBoxScore({ ...apiDate, gameId: gameData.id });
229 |
230 | updatedGameBoxScoreData = _updatedGameBoxScoreData;
231 | } catch (err) {
232 | catchAPIError(err, 'NBA.getBoxScore()');
233 | }
234 |
235 | gamePlayByPlayData = updatedPlayByPlayData;
236 | gameBoxScoreData = updatedGameBoxScoreData;
237 |
238 | const lastPlay = gamePlayByPlayData.play.slice(-1).pop();
239 | homeTeam.setScore(lastPlay.home_score);
240 | visitorTeam.setScore(lastPlay.visitor_score);
241 |
242 | const isFinal =
243 | (lastPlay.period === '4' || +lastPlay.period > 4) &&
244 | lastPlay.description === 'End Period' &&
245 | lastPlay.home_score !== lastPlay.visitor_score;
246 |
247 | live(
248 | homeTeam,
249 | visitorTeam,
250 | {
251 | ...gamePlayByPlayData,
252 | ...seasonMetaData,
253 | isFinal,
254 | },
255 | gameBoxScoreData,
256 | {
257 | screen,
258 | scoreboardTable,
259 | timeText,
260 | homeTeamScoreText,
261 | visitorTeamScoreText,
262 | playByPlayBox,
263 | boxscoreTable,
264 | }
265 | );
266 |
267 | if (isFinal) {
268 | break;
269 | }
270 |
271 | await delay(
272 | gameData.period_time.game_status === 'Halftime' ? 15000 : 3000
273 | );
274 | }
275 | break;
276 | }
277 |
278 | case '3':
279 | default: {
280 | screen.destroy();
281 | console.log('');
282 | scoreboard(homeTeam, visitorTeam, {
283 | ...gameBoxScoreData,
284 | ...seasonMetaData,
285 | });
286 | console.log('');
287 | boxScore(homeTeam, visitorTeam);
288 | }
289 | }
290 | };
291 |
292 | export default game;
293 |
--------------------------------------------------------------------------------
/src/command/game/live.js:
--------------------------------------------------------------------------------
1 | import { getMainColor } from 'nba-color';
2 | import { left, right } from 'wide-align';
3 | import R from 'ramda';
4 | import emoji from 'node-emoji';
5 |
6 | import { bold, nbaRed, neonGreen, colorTeamName } from '../../utils/log';
7 |
8 | const checkOverStandard = (record, standard) =>
9 | +record >= standard ? nbaRed(record) : record;
10 |
11 | const updateTeamQuarterScores = (team, latestPeriod, teamPeriod) => {
12 | // eslint-disable-next-line no-param-reassign
13 | teamPeriod = Array.isArray(teamPeriod) ? teamPeriod : [teamPeriod];
14 |
15 | const latestQuarterScore = teamPeriod.find(
16 | quarter => quarter.period_value === latestPeriod
17 | );
18 |
19 | if (latestQuarterScore && latestQuarterScore.score && latestPeriod) {
20 | if (team.getIsHomeTeam()) {
21 | team.setQuarterScore(latestPeriod, latestQuarterScore.score);
22 | } else {
23 | team.setQuarterScore(latestPeriod, latestQuarterScore.score);
24 | }
25 | }
26 | };
27 |
28 | const getOvertimePeriod = latestPeriod => parseInt(latestPeriod, 10) - 4;
29 |
30 | const getScoreboardTableHeader = latestPeriod => {
31 | const scoreboardTableHeader = ['', 'Q1', 'Q2', 'Q3', 'Q4'];
32 | const overtimePeriod = getOvertimePeriod(latestPeriod);
33 |
34 | for (let i = 0; i < overtimePeriod; i += 1) {
35 | scoreboardTableHeader.push(`OT${overtimePeriod}`);
36 | }
37 |
38 | scoreboardTableHeader.push('Total');
39 | return scoreboardTableHeader;
40 | };
41 |
42 | const getTeamQuarterScores = (team, latestPeriod) => {
43 | const teamQuarterScores = [
44 | `${team.getAbbreviation({
45 | color: true,
46 | })}`,
47 | ];
48 |
49 | for (let i = 1; i <= latestPeriod; i += 1) {
50 | teamQuarterScores.push(bold(team.getQuarterScore(`${i}`)));
51 | }
52 | for (let i = 0; i < 4 - latestPeriod; i += 1) {
53 | teamQuarterScores.push(' ');
54 | }
55 |
56 | teamQuarterScores.push(neonGreen(team.getScore()));
57 |
58 | return teamQuarterScores;
59 | };
60 |
61 | const getPlayByPlayRows = allPlays => {
62 | allPlays.reverse();
63 |
64 | const playByPlayRows = [];
65 |
66 | for (let i = 0; i < allPlays.length; i += 1) {
67 | const {
68 | clock,
69 | period,
70 | description: eventDescription,
71 | home_score,
72 | visitor_score,
73 | team_abr,
74 | } = allPlays[i];
75 |
76 | const overtimePeriod = getOvertimePeriod(period);
77 | const time = `${+overtimePeriod > 1 ? 'OT' : 'Q'}${
78 | +overtimePeriod > 1 ? overtimePeriod : period
79 | } ${clock !== '' ? clock : '12:00'}`;
80 |
81 | const scoreboard = `${right(
82 | home_score > R.prop('home_score', allPlays[i + 1])
83 | ? bold(neonGreen(home_score))
84 | : bold(home_score),
85 | 3
86 | )} - ${left(
87 | visitor_score > R.prop('visitor_score', allPlays[i + 1])
88 | ? bold(neonGreen(visitor_score))
89 | : bold(visitor_score),
90 | 3
91 | )}`;
92 | const teamColor = getMainColor(team_abr)
93 | ? getMainColor(team_abr).hex
94 | : '#000';
95 | const description = `${left(
96 | colorTeamName(teamColor, `${team_abr}`),
97 | 3
98 | )} ${eventDescription.replace(/\[.*\]/i, '')}\n`;
99 |
100 | playByPlayRows.push([time, scoreboard, description].join(' │ '));
101 | }
102 |
103 | return playByPlayRows.join('\n');
104 | };
105 |
106 | const getTeamBoxscore = (team, playersData) => {
107 | const teamBoxscoreRows = [];
108 | teamBoxscoreRows.push([
109 | team.getAbbreviation({ color: true }),
110 | bold('PTS'),
111 | bold('AST'),
112 | bold('REB'),
113 | ]);
114 |
115 | const mainPlayers = playersData
116 | .sort((playerA, playerB) => +playerB.minutes - +playerA.minutes)
117 | .slice(0, 5);
118 |
119 | mainPlayers.forEach(player => {
120 | teamBoxscoreRows.push([
121 | bold(left(player.last_name, 14)),
122 | left(checkOverStandard(player.points, 20), 3),
123 | left(checkOverStandard(player.assists, 10), 3),
124 | left(
125 | `${checkOverStandard(
126 | +player.rebounds_offensive + +player.rebounds_defensive,
127 | 10
128 | )}`,
129 | 3
130 | ),
131 | ]);
132 | });
133 |
134 | return teamBoxscoreRows;
135 | };
136 |
137 | const live = (
138 | homeTeam,
139 | visitorTeam,
140 | playByPlayData,
141 | gameBoxScoreData,
142 | blessedComponents
143 | ) => {
144 | const { play: allPlays, isFinal } = playByPlayData;
145 | const { period: latestPeriod, clock: latestClock } = allPlays.slice(-1).pop();
146 | const {
147 | screen,
148 | scoreboardTable,
149 | timeText,
150 | homeTeamScoreText,
151 | visitorTeamScoreText,
152 | playByPlayBox,
153 | boxscoreTable,
154 | } = blessedComponents;
155 |
156 | const {
157 | home: {
158 | linescores: { period: homeTeamPeriod },
159 | },
160 | visitor: {
161 | linescores: { period: visitorTeamPeriod },
162 | },
163 | } = gameBoxScoreData;
164 |
165 | updateTeamQuarterScores(homeTeam, latestPeriod, homeTeamPeriod);
166 | updateTeamQuarterScores(visitorTeam, latestPeriod, visitorTeamPeriod);
167 |
168 | scoreboardTable.setRows([
169 | getScoreboardTableHeader(latestPeriod),
170 | getTeamQuarterScores(homeTeam, latestPeriod),
171 | getTeamQuarterScores(visitorTeam, latestPeriod),
172 | ]);
173 |
174 | boxscoreTable.setRows([
175 | ...getTeamBoxscore(homeTeam, gameBoxScoreData.home.players.player),
176 | ...getTeamBoxscore(visitorTeam, gameBoxScoreData.visitor.players.player),
177 | ]);
178 |
179 | playByPlayBox.setContent(getPlayByPlayRows(allPlays));
180 | playByPlayBox.focus();
181 |
182 | if (isFinal) {
183 | timeText.setContent(bold('Final'));
184 | } else {
185 | const overtimePeriod = getOvertimePeriod(latestPeriod);
186 | timeText.setContent(
187 | bold(
188 | `${emoji.get('stopwatch')} ${+overtimePeriod > 1 ? 'OT' : 'Q'}${
189 | +overtimePeriod > 1 ? overtimePeriod : latestPeriod
190 | } ${latestClock}`
191 | )
192 | );
193 | }
194 |
195 | homeTeamScoreText.setContent(homeTeam.getScore());
196 | visitorTeamScoreText.setContent(visitorTeam.getScore());
197 |
198 | screen.render();
199 | };
200 |
201 | export default live;
202 |
--------------------------------------------------------------------------------
/src/command/game/network.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 |
3 | const getBroadcastNetworks = televisionNetworks => {
4 | const findNetwork = prop =>
5 | R.find(R.propEq('home_visitor', prop))(televisionNetworks);
6 |
7 | let homeTeamNetwork = findNetwork('home');
8 | let visitorTeamNetwork = findNetwork('visitor');
9 | let nationalNetwork = findNetwork('natl');
10 |
11 | nationalNetwork = !nationalNetwork ? 'N/A' : nationalNetwork.display_name;
12 | homeTeamNetwork = !homeTeamNetwork
13 | ? nationalNetwork
14 | : homeTeamNetwork.display_name;
15 | visitorTeamNetwork = !visitorTeamNetwork
16 | ? nationalNetwork
17 | : visitorTeamNetwork.display_name;
18 |
19 | return {
20 | homeTeam: homeTeamNetwork,
21 | visitorTeam: visitorTeamNetwork,
22 | };
23 | };
24 |
25 | export default getBroadcastNetworks;
26 |
--------------------------------------------------------------------------------
/src/command/game/preview.js:
--------------------------------------------------------------------------------
1 | import format from 'date-fns/format';
2 | import { center } from 'wide-align';
3 | import emoji from 'node-emoji';
4 |
5 | import getBroadcastNetworks from './network';
6 |
7 | import { bold } from '../../utils/log';
8 | import { basicTable } from '../../utils/table';
9 |
10 | const alignCenter = columns =>
11 | columns.map(column => {
12 | if (typeof column === 'string') {
13 | return { content: column, vAlign: 'center', hAlign: 'center' };
14 | }
15 |
16 | return { ...column, vAlign: 'center', hAlign: 'center' };
17 | });
18 |
19 | const createTeamStatsColumns = (
20 | teamName,
21 | {
22 | gp,
23 | w,
24 | l,
25 | pts,
26 | fgPct,
27 | fg3Pct,
28 | ftPct,
29 | oreb,
30 | dreb,
31 | reb,
32 | ast,
33 | blk,
34 | stl,
35 | tov,
36 | pf,
37 | plusMinus,
38 | }
39 | ) => [
40 | teamName,
41 | `${w} - ${l}`,
42 | (w / gp).toFixed(3),
43 | `${pts}`,
44 | `${(fgPct * 100).toFixed(1)}`,
45 | `${(fg3Pct * 100).toFixed(1)}`,
46 | `${(ftPct * 100).toFixed(1)}`,
47 | `${oreb}`,
48 | `${dreb}`,
49 | `${reb}`,
50 | `${ast}`,
51 | `${blk}`,
52 | `${stl}`,
53 | `${tov}`,
54 | `${pf}`,
55 | `${plusMinus}`,
56 | ];
57 |
58 | const preview = (
59 | homeTeam,
60 | visitorTeam,
61 | {
62 | date,
63 | time,
64 | arena,
65 | city,
66 | state,
67 | display_year,
68 | display_season,
69 | broadcasters,
70 | homeTeamDashboardData,
71 | visitorTeamDashboardData,
72 | }
73 | ) => {
74 | const gamePreviewTable = basicTable();
75 | const columnMaxWidth = Math.max(
76 | homeTeam.getFullName({ color: false }).length,
77 | visitorTeam.getFullName({ color: false }).length
78 | );
79 | const networks = getBroadcastNetworks(broadcasters.tv.broadcaster);
80 |
81 | gamePreviewTable.push(
82 | alignCenter([
83 | {
84 | colSpan: 16,
85 | content: bold(`${display_year} ${display_season}`),
86 | },
87 | ]),
88 | alignCenter([
89 | {
90 | colSpan: 16,
91 | content: bold(
92 | `${emoji.get('calendar')} ${format(date, 'YYYY/MM/DD')} ${time.slice(
93 | 0,
94 | 2
95 | )}:${time.slice(2, 4)}`
96 | ),
97 | },
98 | ]),
99 | alignCenter([
100 | {
101 | colSpan: 16,
102 | content: bold(`${emoji.get('house')} ${arena} | ${city}, ${state}`),
103 | },
104 | ]),
105 | alignCenter([
106 | {
107 | colSpan: 16,
108 | content: bold(
109 | `${networks.homeTeam} ${emoji.get('tv')} ${networks.visitorTeam}`
110 | ),
111 | },
112 | ]),
113 | alignCenter(
114 | createTeamStatsColumns(
115 | center(homeTeam.getFullName({ color: true }), columnMaxWidth),
116 | homeTeamDashboardData
117 | )
118 | ),
119 | alignCenter([
120 | '',
121 | bold('RECORD'),
122 | bold('WIN%'),
123 | bold('PTS'),
124 | bold('FG%'),
125 | bold('3P%'),
126 | bold('FT%'),
127 | bold('OREB'),
128 | bold('DREB'),
129 | bold('REB'),
130 | bold('AST'),
131 | bold('BLK'),
132 | bold('STL'),
133 | bold('TOV'),
134 | bold('PF'),
135 | bold('+/-'),
136 | ]),
137 | alignCenter(
138 | createTeamStatsColumns(
139 | center(visitorTeam.getFullName({ color: true }), columnMaxWidth),
140 | visitorTeamDashboardData
141 | )
142 | )
143 | );
144 |
145 | console.log(gamePreviewTable.toString());
146 | };
147 |
148 | export default preview;
149 |
--------------------------------------------------------------------------------
/src/command/game/schedule.js:
--------------------------------------------------------------------------------
1 | import inquirer from 'inquirer';
2 | import emoji from 'node-emoji';
3 | import { limit } from 'stringz';
4 | import { center, left, right } from 'wide-align';
5 | import pMap from 'p-map';
6 | import ora from 'ora';
7 |
8 | import getBroadcastNetworks from './network';
9 | import Team from '../Team';
10 |
11 | import NBA from '../../utils/nba';
12 | import { bold, neonGreen } from '../../utils/log';
13 | import catchAPIError from '../../utils/catchAPIError';
14 |
15 | const MAX_WIDTH = 81;
16 | const TEAMNAME_WIDTH = 20;
17 | const STATUS_WIDTH = 18;
18 | const NETWORK_WIDTH = 35;
19 | const MAX_WIDTH_WITH_NETWORKS = 156;
20 |
21 | const padHomeTeamName = name => bold(right(name, TEAMNAME_WIDTH));
22 | const padVisitorTeamName = name => bold(left(name, TEAMNAME_WIDTH));
23 | const padGameStatus = status => center(status, STATUS_WIDTH);
24 | const padHomeTeamNetwork = network => bold(right(network, NETWORK_WIDTH));
25 | const padAwayTeamNetwork = network => bold(left(network, NETWORK_WIDTH));
26 |
27 | const createGameChoice = (homeTeam, visitorTeam, periodTime, broadcasters) => {
28 | let winner = '';
29 | const { period_status: periodStatus, game_clock: gameClock } = periodTime;
30 |
31 | if (+homeTeam.getScore() > +visitorTeam.getScore()) {
32 | winner = 'home';
33 | } else if (+homeTeam.getScore() === +visitorTeam.getScore()) {
34 | winner = null;
35 | } else {
36 | winner = 'visitor';
37 | }
38 |
39 | const homeTeamName = padHomeTeamName(
40 | winner === 'home'
41 | ? homeTeam.getWinnerName('left')
42 | : homeTeam.getName({ color: true })
43 | );
44 | const visitorTeamName = padVisitorTeamName(
45 | winner === 'visitor'
46 | ? visitorTeam.getWinnerName('right')
47 | : visitorTeam.getName({ color: true })
48 | );
49 | const match = `${homeTeamName}${center(
50 | emoji.get('basketball'),
51 | 8
52 | )}${visitorTeamName}`;
53 | const homeTeamScore =
54 | winner === 'home'
55 | ? right(bold(neonGreen(homeTeam.getScore())), 4)
56 | : right(bold(homeTeam.getScore()), 4);
57 | const visitorTeamScore =
58 | winner === 'visitor'
59 | ? left(bold(neonGreen(visitorTeam.getScore())), 4)
60 | : left(bold(visitorTeam.getScore()), 4);
61 | const score = `${homeTeamScore} : ${visitorTeamScore}`;
62 |
63 | if (broadcasters) {
64 | const networks = getBroadcastNetworks(broadcasters.tv.broadcaster);
65 | const networksOutput = `${padHomeTeamNetwork(
66 | networks.homeTeam
67 | )} ${emoji.get('tv')} ${padAwayTeamNetwork(networks.visitorTeam)}|`;
68 | return `│⌘${match}│${score}│${padGameStatus(
69 | `${bold(periodStatus)} ${gameClock}`
70 | )}│${networksOutput}`;
71 | }
72 | return `│⌘${match}│${score}│${padGameStatus(
73 | `${bold(periodStatus)} ${gameClock}`
74 | )}│`;
75 | };
76 |
77 | const getTeamInfo = async (team, seasonId) => {
78 | try {
79 | const { teamInfoCommon: teamInfo } = await NBA.teamInfoCommon({
80 | TeamID: team.id,
81 | Season: seasonId,
82 | });
83 |
84 | return new Team({
85 | ...teamInfo[0],
86 | score: team.score,
87 | linescores: team.linescores,
88 | isHomeTeam: true,
89 | });
90 | } catch (err) {
91 | catchAPIError(err, 'NBA.teamInfoCommon()');
92 | }
93 | };
94 |
95 | const chooseGameFromSchedule = async (gamesData, option) => {
96 | const spinner = ora(
97 | `Loading Game Schedule...(0/${gamesData.length})`
98 | ).start();
99 | let networksHeader = '';
100 |
101 | if (option.networks) {
102 | networksHeader = `${padHomeTeamNetwork('Home')} ${emoji.get(
103 | 'tv'
104 | )} ${padAwayTeamNetwork('Away')}|`;
105 | }
106 |
107 | const header = `│ ${padHomeTeamName('Home')}${center(
108 | emoji.get('basketball'),
109 | 8
110 | )}${padVisitorTeamName('Away')}│${center('Score', 11)}│${padGameStatus(
111 | 'Status'
112 | )}│${networksHeader}`;
113 |
114 | const tableWidth = !option.networks ? MAX_WIDTH : MAX_WIDTH_WITH_NETWORKS;
115 | const questions = [
116 | {
117 | name: 'game',
118 | message: 'Which game do you want to watch?',
119 | type: 'list',
120 | pageSize: 30,
121 | choices: [
122 | new inquirer.Separator(`${limit('', tableWidth, '─')}`),
123 | new inquirer.Separator(header),
124 | new inquirer.Separator(`${limit('', tableWidth, '─')}`),
125 | ],
126 | },
127 | ];
128 |
129 | const last = gamesData.length - 1;
130 |
131 | await pMap(
132 | gamesData,
133 | async (gameData, index) => {
134 | const { home, visitor, period_time, broadcasters } = gameData;
135 | const homeTeam = await getTeamInfo(home, process.env.season);
136 | const visitorTeam = await getTeamInfo(visitor, process.env.season);
137 |
138 | spinner.text = `Loading Game Schedule...(${index + 1}/${
139 | gamesData.length
140 | })`;
141 |
142 | questions[0].choices.push({
143 | name: createGameChoice(
144 | homeTeam,
145 | visitorTeam,
146 | period_time,
147 | !option.networks ? null : broadcasters
148 | ),
149 | value: { gameData, homeTeam, visitorTeam },
150 | });
151 |
152 | if (index !== last) {
153 | questions[0].choices.push(
154 | new inquirer.Separator(`${limit('', tableWidth, '─')}`)
155 | );
156 | } else {
157 | questions[0].choices.push(
158 | new inquirer.Separator(`${limit('', tableWidth, '─')}`)
159 | );
160 | }
161 | },
162 | { concurrency: 1 }
163 | );
164 |
165 | spinner.stop();
166 |
167 | const answer = await inquirer.prompt(questions);
168 |
169 | return answer;
170 | };
171 |
172 | export default chooseGameFromSchedule;
173 | export { getTeamInfo };
174 |
--------------------------------------------------------------------------------
/src/command/game/scoreboard.js:
--------------------------------------------------------------------------------
1 | import format from 'date-fns/format';
2 | import emoji from 'node-emoji';
3 | import { center } from 'wide-align';
4 |
5 | import getBroadcastNetworks from './network';
6 |
7 | import { bold, nbaRed, neonGreen } from '../../utils/log';
8 | import { basicTable } from '../../utils/table';
9 |
10 | const vAlignCenter = columns =>
11 | columns.map(column => {
12 | if (typeof column === 'string') {
13 | return { content: column, vAlign: 'center', hAlign: 'center' };
14 | }
15 |
16 | return { ...column, vAlign: 'center' };
17 | });
18 |
19 | const getStartingPlayers = team =>
20 | team
21 | .getPlayers()
22 | .filter(
23 | player => player.starting_position !== '' || player.on_court === '1'
24 | )
25 | .map(player => ({
26 | name: `${player.first_name} ${player.last_name}`,
27 | position: player.starting_position || player.position_short,
28 | }));
29 |
30 | const teamGameLeaders = (homeTeam, visitorTeam, field) =>
31 | vAlignCenter([
32 | {
33 | colSpan: 3,
34 | content: bold(
35 | `${homeTeam.getGameLeaders(field).leader[0].FirstName} ${
36 | homeTeam.getGameLeaders(field).leader[0].LastName
37 | }`
38 | ),
39 | hAlign: 'right',
40 | },
41 | nbaRed(homeTeam.getGameLeaders(field).StatValue),
42 | {
43 | colSpan: 2,
44 | content: field,
45 | hAlign: 'center',
46 | },
47 | nbaRed(visitorTeam.getGameLeaders(field).StatValue),
48 | {
49 | colSpan: 3,
50 | content: bold(
51 | `${visitorTeam.getGameLeaders(field).leader[0].FirstName} ${
52 | visitorTeam.getGameLeaders(field).leader[0].LastName
53 | }`
54 | ),
55 | hAlign: 'left',
56 | },
57 | ]);
58 |
59 | const scoreboard = (
60 | homeTeam,
61 | visitorTeam,
62 | { date, time, arena, city, state, display_year, display_season, broadcasters }
63 | ) => {
64 | const scoreboardTable = basicTable();
65 |
66 | const formatedTime = `${time.slice(0, 2)}:${time.slice(2, 4)}`;
67 |
68 | const homeTeamStartingPlayers = getStartingPlayers(homeTeam);
69 | const visitorTeamStartingPlayers = getStartingPlayers(visitorTeam);
70 |
71 | const networks = getBroadcastNetworks(broadcasters.tv.broadcaster);
72 |
73 | scoreboardTable.push(
74 | vAlignCenter([
75 | {
76 | colSpan: 10,
77 | content: bold(`${display_year} ${display_season}`),
78 | hAlign: 'center',
79 | },
80 | ]),
81 | vAlignCenter([
82 | {
83 | colSpan: 2,
84 | content: bold(homeTeam.getName({ color: true })),
85 | hAlign: 'center',
86 | },
87 | {
88 | colSpan: 6,
89 | content: bold('Final'),
90 | hAlign: 'center',
91 | },
92 | {
93 | colSpan: 2,
94 | content: bold(visitorTeam.getName({ color: true })),
95 | hAlign: 'center',
96 | },
97 | ]),
98 | vAlignCenter([
99 | 'PG',
100 | {
101 | content: bold(
102 | homeTeamStartingPlayers.filter(
103 | player => player.position.indexOf('G') > -1
104 | )[1].name
105 | ),
106 | hAlign: 'left',
107 | },
108 | bold('Team'),
109 | bold('Q1'),
110 | bold('Q2'),
111 | bold('Q3'),
112 | bold('Q4'), // FIXME OT
113 | bold(center('Total', 9)),
114 | 'PG',
115 | {
116 | content: bold(
117 | visitorTeamStartingPlayers.filter(
118 | player => player.position.indexOf('G') > -1
119 | )[1].name
120 | ),
121 | hAlign: 'left',
122 | },
123 | ]),
124 | vAlignCenter([
125 | 'SG',
126 | {
127 | content: bold(
128 | homeTeamStartingPlayers.filter(
129 | player => player.position.indexOf('G') > -1
130 | )[0].name
131 | ),
132 | hAlign: 'left',
133 | },
134 | `${homeTeam.getAbbreviation({
135 | color: true,
136 | })} (${homeTeam.getWins()}-${homeTeam.getLoses()})`,
137 | bold(homeTeam.getQuarterScore('1')),
138 | bold(homeTeam.getQuarterScore('2')),
139 | bold(homeTeam.getQuarterScore('3')),
140 | bold(homeTeam.getQuarterScore('4')),
141 | bold(neonGreen(homeTeam.getScore())),
142 | 'SG',
143 | {
144 | content: bold(
145 | visitorTeamStartingPlayers.filter(
146 | player => player.position.indexOf('G') > -1
147 | )[0].name
148 | ),
149 | hAlign: 'left',
150 | },
151 | ]),
152 | vAlignCenter([
153 | 'SF',
154 | {
155 | content: bold(
156 | homeTeamStartingPlayers.filter(
157 | player => player.position.indexOf('F') > -1
158 | )[1].name
159 | ),
160 | hAlign: 'left',
161 | },
162 | `${visitorTeam.getAbbreviation({
163 | color: true,
164 | })} (${visitorTeam.getWins()}-${visitorTeam.getLoses()})`,
165 | bold(visitorTeam.getQuarterScore('1')),
166 | bold(visitorTeam.getQuarterScore('2')),
167 | bold(visitorTeam.getQuarterScore('3')),
168 | bold(visitorTeam.getQuarterScore('4')),
169 | bold(neonGreen(visitorTeam.getScore())),
170 | 'SF',
171 | {
172 | content: bold(
173 | visitorTeamStartingPlayers.filter(
174 | player => player.position.indexOf('F') > -1
175 | )[1].name
176 | ),
177 | hAlign: 'left',
178 | },
179 | ]),
180 | vAlignCenter([
181 | 'PF',
182 | {
183 | content: bold(
184 | homeTeamStartingPlayers.filter(
185 | player => player.position.indexOf('F') > -1
186 | )[0].name
187 | ),
188 | hAlign: 'left',
189 | },
190 | {
191 | colSpan: 6,
192 | content: bold(
193 | `${emoji.get('calendar')} ${format(
194 | date,
195 | 'YYYY/MM/DD'
196 | )} ${formatedTime}`
197 | ),
198 | hAlign: 'center',
199 | },
200 | 'PF',
201 | {
202 | content: bold(
203 | visitorTeamStartingPlayers.filter(
204 | player => player.position.indexOf('F') > -1
205 | )[0].name
206 | ),
207 | hAlign: 'left',
208 | },
209 | ]),
210 | vAlignCenter([
211 | 'C',
212 | {
213 | content: bold(
214 | homeTeamStartingPlayers.find(player => player.position === 'C').name
215 | ),
216 | hAlign: 'left',
217 | },
218 | {
219 | colSpan: 6,
220 | content: bold(`${emoji.get('house')} ${arena} │ ${city}, ${state}`),
221 | hAlign: 'center',
222 | },
223 | 'C',
224 | {
225 | content: bold(
226 | visitorTeamStartingPlayers.find(player => player.position === 'C')
227 | .name
228 | ),
229 | hAlign: 'left',
230 | },
231 | ]),
232 | vAlignCenter([
233 | {
234 | colSpan: 10,
235 | content: bold(
236 | `${networks.homeTeam} ${emoji.get('tv')} ${networks.visitorTeam}`
237 | ),
238 | hAlign: 'center',
239 | },
240 | ]),
241 | vAlignCenter([
242 | {
243 | colSpan: 10,
244 | content: bold('Game Record Leaders'),
245 | hAlign: 'center',
246 | },
247 | ]),
248 | teamGameLeaders(homeTeam, visitorTeam, 'Points'),
249 | teamGameLeaders(homeTeam, visitorTeam, 'Assists'),
250 | teamGameLeaders(homeTeam, visitorTeam, 'Rebounds')
251 | );
252 |
253 | console.log(scoreboardTable.toString());
254 | };
255 |
256 | export default scoreboard;
257 |
--------------------------------------------------------------------------------
/src/command/index.js:
--------------------------------------------------------------------------------
1 | export { default as player } from './player';
2 | export { default as game } from './game';
3 |
--------------------------------------------------------------------------------
/src/command/player/index.js:
--------------------------------------------------------------------------------
1 | import pMap from 'p-map';
2 | import emoji from 'node-emoji';
3 |
4 | import playerInfo from './info';
5 | import seasonStats from './seasonStats';
6 | import playerInfoCompare from './infoCompare';
7 |
8 | import NBA from '../../utils/nba';
9 | import catchAPIError from '../../utils/catchAPIError';
10 | import seasonStatsCompare from './seasonStatsCompare';
11 |
12 | const player = async (playerName, option) => {
13 | await NBA.updatePlayers();
14 |
15 | const nameArray = playerName.split(',');
16 |
17 | const [_players] = await pMap(nameArray, async name => {
18 | const result = await NBA.searchPlayers(name.trim());
19 |
20 | return result;
21 | });
22 |
23 | if (option.compare) {
24 | let playerDataArr;
25 |
26 | try {
27 | playerDataArr = await pMap(_players, async _player => {
28 | const result = await NBA.playerInfo({ PlayerID: _player.playerId });
29 |
30 | return result;
31 | });
32 | } catch (err) {
33 | catchAPIError(err, 'NBA.playerInfo()');
34 | }
35 |
36 | if (option.info) {
37 | playerInfoCompare(playerDataArr);
38 | }
39 |
40 | if (option.regular || option.playoffs) {
41 | let playerProfileArr;
42 |
43 | try {
44 | playerProfileArr = await pMap(_players, async _player => {
45 | const result = NBA.playerProfile({ PlayerID: _player.playerId });
46 |
47 | return result;
48 | });
49 | } catch (err) {
50 | catchAPIError(err, 'NBA.playerProfile()');
51 | }
52 |
53 | if (option.regular) {
54 | seasonStatsCompare(playerProfileArr, playerDataArr, 'Regular Season');
55 | }
56 |
57 | if (option.playoffs) {
58 | seasonStatsCompare(playerProfileArr, playerDataArr, 'Post Season');
59 | }
60 | }
61 | } else {
62 | pMap(
63 | _players,
64 | async _player => {
65 | let commonPlayerInfo;
66 | let playerHeadlineStats;
67 |
68 | try {
69 | const {
70 | commonPlayerInfo: _commonPlayerInfo,
71 | playerHeadlineStats: _playerHeadlineStats,
72 | } = await NBA.playerInfo({
73 | PlayerID: _player.playerId,
74 | });
75 |
76 | commonPlayerInfo = _commonPlayerInfo;
77 | playerHeadlineStats = _playerHeadlineStats;
78 | } catch (err) {
79 | catchAPIError(err, 'NBA.playerInfo()');
80 | }
81 |
82 | if (option.info) {
83 | playerInfo({ ...commonPlayerInfo[0], ...playerHeadlineStats[0] });
84 | }
85 |
86 | if (option.regular) {
87 | let seasonTotalsRegularSeason;
88 | let careerTotalsRegularSeason;
89 |
90 | try {
91 | const {
92 | seasonTotalsRegularSeason: _seasonTotalsRegularSeason,
93 | careerTotalsRegularSeason: _careerTotalsRegularSeason,
94 | } = await NBA.playerProfile({
95 | PlayerID: _player.playerId,
96 | });
97 |
98 | seasonTotalsRegularSeason = _seasonTotalsRegularSeason;
99 | careerTotalsRegularSeason = _careerTotalsRegularSeason;
100 | } catch (err) {
101 | catchAPIError(err, 'NBA.playerProfile()');
102 | }
103 |
104 | commonPlayerInfo[0].nowTeamAbbreviation =
105 | commonPlayerInfo[0].teamAbbreviation;
106 |
107 | seasonStats({
108 | seasonType: 'Regular Season',
109 | ...commonPlayerInfo[0],
110 | seasonTotals: seasonTotalsRegularSeason,
111 | careerTotals: careerTotalsRegularSeason[0],
112 | });
113 | }
114 |
115 | if (option.playoffs) {
116 | let seasonTotalsPostSeason;
117 | let careerTotalsPostSeason;
118 |
119 | try {
120 | const {
121 | seasonTotalsPostSeason: _seasonTotalsPostSeason,
122 | careerTotalsPostSeason: _careerTotalsPostSeason,
123 | } = await NBA.playerProfile({
124 | PlayerID: _player.playerId,
125 | });
126 |
127 | seasonTotalsPostSeason = _seasonTotalsPostSeason;
128 | careerTotalsPostSeason = _careerTotalsPostSeason;
129 | } catch (err) {
130 | catchAPIError(err, 'NBA.playerProfile()');
131 | }
132 |
133 | if (careerTotalsPostSeason.length === 0) {
134 | console.log(
135 | `Sorry, ${_player.firstName} ${
136 | _player.lastName
137 | } doesn't have any playoffs data ${emoji.get('confused')}`
138 | );
139 | } else {
140 | commonPlayerInfo[0].nowTeamAbbreviation =
141 | commonPlayerInfo[0].teamAbbreviation;
142 |
143 | seasonStats({
144 | seasonType: 'Playoffs',
145 | ...commonPlayerInfo[0],
146 | seasonTotals: seasonTotalsPostSeason,
147 | careerTotals: careerTotalsPostSeason[0],
148 | });
149 | }
150 | }
151 | },
152 | { concurrency: 1 }
153 | );
154 | }
155 | };
156 |
157 | export default player;
158 |
--------------------------------------------------------------------------------
/src/command/player/info.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 | import format from 'date-fns/format';
3 | import { getMainColor } from 'nba-color';
4 |
5 | import { convertToCm, convertToKg } from '../../utils/convertUnit';
6 | import { basicTable } from '../../utils/table';
7 | import { bold } from '../../utils/log';
8 |
9 | const alignCenter = columns =>
10 | columns.map(content => ({ content, hAlign: 'center', vAlign: 'center' }));
11 |
12 | const info = playerInfo => {
13 | const playerTable = basicTable();
14 | const {
15 | teamAbbreviation,
16 | jersey,
17 | displayFirstLast,
18 | height,
19 | weight,
20 | country,
21 | birthdate,
22 | seasonExp,
23 | draftYear,
24 | draftRound,
25 | draftNumber,
26 | pts,
27 | reb,
28 | ast,
29 | } = playerInfo;
30 |
31 | const teamMainColor = getMainColor(teamAbbreviation);
32 | const playerName = chalk`{bold.white.bgHex('${
33 | teamMainColor ? teamMainColor.hex : '#000'
34 | }') ${teamAbbreviation}} {bold.white #${jersey} ${displayFirstLast}}`;
35 |
36 | const draft =
37 | draftYear !== 'Undrafted'
38 | ? `${draftYear} Rnd ${draftRound} Pick ${draftNumber}`
39 | : 'Undrafted';
40 |
41 | playerTable.push(
42 | [{ colSpan: 9, content: playerName, hAlign: 'center', vAlign: 'center' }],
43 | alignCenter([
44 | bold('Height'),
45 | bold('Weight'),
46 | bold('Country'),
47 | bold('Born'),
48 | bold('EXP'),
49 | bold('Draft'),
50 | bold('PTS'),
51 | bold('REB'),
52 | bold('AST'),
53 | ]),
54 | alignCenter([
55 | `${height} / ${convertToCm(height)}`,
56 | `${weight} / ${convertToKg(weight)}`,
57 | country,
58 | `${format(birthdate, 'YYYY/MM/DD')}`,
59 | `${seasonExp} yrs`,
60 | draft,
61 | pts,
62 | reb,
63 | ast,
64 | ])
65 | );
66 |
67 | console.log(playerTable.toString());
68 | };
69 |
70 | export default info;
71 |
--------------------------------------------------------------------------------
/src/command/player/infoCompare.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 | import format from 'date-fns/format';
3 | import { getMainColor } from 'nba-color';
4 |
5 | import { convertToCm, convertToKg } from '../../utils/convertUnit';
6 | import { basicTable } from '../../utils/table';
7 | import { bold } from '../../utils/log';
8 |
9 | const alignCenter = columns =>
10 | columns.map(content => ({ content, hAlign: 'center', vAlign: 'center' }));
11 |
12 | const infoCompare = playerInfo => {
13 | const playerTable = basicTable();
14 | let nameStr = '';
15 |
16 | playerInfo.forEach(elem => {
17 | const {
18 | teamAbbreviation,
19 | jersey,
20 | displayFirstLast,
21 | height,
22 | weight,
23 | country,
24 | birthdate,
25 | seasonExp,
26 | draftYear,
27 | draftRound,
28 | draftNumber,
29 | pts,
30 | reb,
31 | ast,
32 | } = { ...elem.commonPlayerInfo[0], ...elem.playerHeadlineStats[0] };
33 |
34 | const teamMainColor = getMainColor(teamAbbreviation);
35 | const playerName = chalk`{bold.white.bgHex('${
36 | teamMainColor ? teamMainColor.hex : '#000'
37 | }') ${teamAbbreviation}} {bold.white #${jersey} ${displayFirstLast}}`;
38 |
39 | const draft =
40 | draftYear !== 'Undrafted'
41 | ? `${draftYear} Rnd ${draftRound} Pick ${draftNumber}`
42 | : 'Undrafted';
43 |
44 | nameStr += `${playerName}\n`;
45 |
46 | playerTable.push(
47 | alignCenter([
48 | `${height} / ${convertToCm(height)}`,
49 | `${weight} / ${convertToKg(weight)}`,
50 | country,
51 | `${format(birthdate, 'YYYY/MM/DD')}`,
52 | `${seasonExp} yrs`,
53 | draft,
54 | pts,
55 | reb,
56 | ast,
57 | ])
58 | );
59 | });
60 |
61 | playerTable.unshift(
62 | [
63 | {
64 | colSpan: 9,
65 | content: nameStr.trim(),
66 | hAlign: 'center',
67 | vAlign: 'center',
68 | },
69 | ],
70 | alignCenter([
71 | bold('Height'),
72 | bold('Weight'),
73 | bold('Country'),
74 | bold('Born'),
75 | bold('EXP'),
76 | bold('Draft'),
77 | bold('PTS'),
78 | bold('REB'),
79 | bold('AST'),
80 | ])
81 | );
82 | console.log(playerTable.toString());
83 | };
84 |
85 | export default infoCompare;
86 |
--------------------------------------------------------------------------------
/src/command/player/seasonStats.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 | import { getMainColor } from 'nba-color';
3 |
4 | import { bold } from '../../utils/log';
5 | import { basicTable } from '../../utils/table';
6 |
7 | const alignCenter = columns =>
8 | columns.map(content => ({ content, hAlign: 'center', vAlign: 'center' }));
9 |
10 | const seasonStats = ({
11 | seasonType,
12 | nowTeamAbbreviation,
13 | jersey,
14 | displayFirstLast,
15 | seasonTotals,
16 | careerTotals,
17 | }) => {
18 | const nowTeamMainColor = getMainColor(nowTeamAbbreviation);
19 | const seasonTable = basicTable();
20 | const playerName = chalk`{bold.white.bgHex('${
21 | nowTeamMainColor ? nowTeamMainColor.hex : '#000'
22 | }') ${nowTeamAbbreviation}} {bold.white #${jersey} ${displayFirstLast} │ ${seasonType}}`;
23 |
24 | seasonTable.push([{ colSpan: 14, content: playerName, hAlign: 'center' }]);
25 | seasonTable.push(
26 | alignCenter([
27 | bold('SEASON'),
28 | bold('TEAM'),
29 | bold('AGE'),
30 | bold('GP'),
31 | bold('MIN'),
32 | bold('PTS'),
33 | bold('FG%'),
34 | bold('3P%'),
35 | bold('FT%'),
36 | bold('AST'),
37 | bold('REB'),
38 | bold('STL'),
39 | bold('BLK'),
40 | bold('TOV'),
41 | ])
42 | );
43 |
44 | seasonTotals.reverse().forEach(season => {
45 | const {
46 | seasonId,
47 | teamAbbreviation,
48 | playerAge,
49 | gp,
50 | min,
51 | pts,
52 | fgPct,
53 | fg3Pct,
54 | ftPct,
55 | ast,
56 | reb,
57 | stl,
58 | blk,
59 | tov,
60 | } = season;
61 | const teamMainColor = getMainColor(teamAbbreviation);
62 |
63 | seasonTable.push(
64 | alignCenter([
65 | bold(seasonId),
66 | chalk`{bold.white.bgHex('${
67 | teamMainColor ? teamMainColor.hex : '#000'
68 | }') ${teamAbbreviation}}`,
69 | playerAge,
70 | gp,
71 | min,
72 | pts,
73 | (fgPct * 100).toFixed(1),
74 | (fg3Pct * 100).toFixed(1),
75 | (ftPct * 100).toFixed(1),
76 | ast,
77 | reb,
78 | stl,
79 | blk,
80 | tov,
81 | ])
82 | );
83 | });
84 |
85 | const {
86 | gp,
87 | min,
88 | pts,
89 | fgPct,
90 | fg3Pct,
91 | ftPct,
92 | ast,
93 | reb,
94 | stl,
95 | blk,
96 | tov,
97 | } = careerTotals;
98 |
99 | seasonTable.push(
100 | alignCenter([
101 | bold('Overall'),
102 | bold(''),
103 | bold(''),
104 | bold(gp),
105 | bold(min),
106 | bold(pts),
107 | bold((fgPct * 100).toFixed(1)),
108 | bold((fg3Pct * 100).toFixed(1)),
109 | bold((ftPct * 100).toFixed(1)),
110 | bold(ast),
111 | bold(reb),
112 | bold(stl),
113 | bold(blk),
114 | bold(tov),
115 | ])
116 | );
117 |
118 | console.log(seasonTable.toString());
119 | };
120 |
121 | export default seasonStats;
122 |
--------------------------------------------------------------------------------
/src/command/player/seasonStatsCompare.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import chalk from 'chalk';
3 | import { getMainColor } from 'nba-color';
4 |
5 | import { bold } from '../../utils/log';
6 | import { basicTable } from '../../utils/table';
7 |
8 | const alignCenter = columns =>
9 | columns.map(content => ({ content, hAlign: 'center', vAlign: 'center' }));
10 |
11 | const findMaxInd = arr => {
12 | let maxInd = 0;
13 | for (let i = 1; i < arr.length; i += 1) {
14 | if (arr[i] !== '-') {
15 | if (arr[i] > arr[maxInd]) {
16 | maxInd = i;
17 | }
18 | }
19 | }
20 | return maxInd;
21 | };
22 |
23 | const makeNameStr = (playerInfo, seasonType) => {
24 | let nameStr = '';
25 | playerInfo.forEach(player => {
26 | const { teamAbbreviation, jersey, displayFirstLast } = {
27 | ...player.commonPlayerInfo[0],
28 | };
29 | const teamMainColor = getMainColor(teamAbbreviation);
30 | const playerName = chalk`{bold.white.bgHex('${
31 | teamMainColor ? teamMainColor.hex : '#000'
32 | }') ${teamAbbreviation}} {bold.white #${jersey} ${displayFirstLast} │ ${seasonType}}`;
33 | nameStr += `${playerName}\n`;
34 | });
35 | return nameStr;
36 | };
37 |
38 | const makeSeasonObj = (playerProfile, seasonStr) => {
39 | const seasonObj = {};
40 | let index = 0;
41 | playerProfile.forEach(player => {
42 | const seasonArr = player[`seasonTotals${seasonStr}`];
43 | seasonArr.forEach(season => {
44 | const currentSeason = season.seasonId;
45 | if (seasonObj[currentSeason]) {
46 | seasonObj[currentSeason][index] = season;
47 | } else {
48 | seasonObj[currentSeason] = [...Array(playerProfile.length)].fill({});
49 | seasonObj[currentSeason][index] = season;
50 | }
51 | });
52 | index += 1;
53 | });
54 | return seasonObj;
55 | };
56 |
57 | const makeOverall = (playerProfile, seasonStr) => {
58 | const overallArr = [];
59 | playerProfile.forEach(player => {
60 | overallArr.push(...player[`careerTotals${seasonStr}`]);
61 | });
62 | return overallArr;
63 | };
64 |
65 | /* eslint-disable no-param-reassign */
66 | const makeRow = seasonData => {
67 | const template = {
68 | teamAbbreviation: [],
69 | playerAge: [],
70 | gp: [],
71 | min: [],
72 | pts: [],
73 | fgPct: [],
74 | fg3Pct: [],
75 | ftPct: [],
76 | ast: [],
77 | reb: [],
78 | stl: [],
79 | blk: [],
80 | tov: [],
81 | };
82 | let seasonId;
83 |
84 | seasonData.forEach(player => {
85 | if (Object.keys(player).length !== 0) {
86 | player.fg3Pct = (player.fg3Pct * 100).toFixed(1);
87 | player.fgPct = (player.fgPct * 100).toFixed(1);
88 | player.ftPct = (player.ftPct * 100).toFixed(1);
89 | if (player.teamAbbreviation) {
90 | const teamMainColor = getMainColor(player.teamAbbreviation);
91 | player.teamAbbreviation = chalk`{bold.white.bgHex('${
92 | teamMainColor ? teamMainColor.hex : '#000'
93 | }') ${player.teamAbbreviation}}`;
94 | }
95 | if (!template.seasonId) {
96 | seasonId = bold(player.seasonId);
97 | }
98 | }
99 |
100 | const templatePusher = (val, key) => {
101 | template[key].push(player[key] || '-');
102 | };
103 |
104 | R.forEachObjIndexed(templatePusher, template);
105 | });
106 |
107 | const colorStats = (val, key) => {
108 | const maxInd = findMaxInd(template[key]);
109 | template[key][maxInd] = chalk.green(template[key][maxInd]);
110 | template[key] = template[key].join('\n');
111 | };
112 |
113 | R.forEachObjIndexed(colorStats, template);
114 |
115 | template.seasonId = seasonId;
116 | return template;
117 | };
118 |
119 | /* eslint-enable no-param-reassign */
120 | const seasonStatsCompare = (
121 | playerProfile,
122 | playerInfo,
123 | seasonType = 'Regular Season'
124 | ) => {
125 | const seasonStr = seasonType.replace(/\s/g, '');
126 |
127 | const seasonObj = makeSeasonObj(playerProfile, seasonStr);
128 | const overallArr = makeOverall(playerProfile, seasonStr);
129 | const nameStr = makeNameStr(playerInfo, seasonType);
130 |
131 | const sorter = (a, b) => {
132 | const aDate = a.split('-')[0];
133 | const bDate = b.split('-')[0];
134 | return aDate - bDate;
135 | };
136 | const seasonDates = R.sort(sorter, R.keys(seasonObj));
137 |
138 | const seasonTable = basicTable();
139 |
140 | seasonTable.push([
141 | { colSpan: 14, content: nameStr.trim(), hAlign: 'center' },
142 | ]);
143 | seasonTable.push(
144 | alignCenter([
145 | bold('SEASON'),
146 | bold('TEAM'),
147 | bold('AGE'),
148 | bold('GP'),
149 | bold('MIN'),
150 | bold('PTS'),
151 | bold('FG%'),
152 | bold('3P%'),
153 | bold('FT%'),
154 | bold('AST'),
155 | bold('REB'),
156 | bold('STL'),
157 | bold('BLK'),
158 | bold('TOV'),
159 | ])
160 | );
161 |
162 | seasonDates.reverse().forEach(key => {
163 | const row = makeRow(seasonObj[key]);
164 |
165 | seasonTable.push(
166 | alignCenter([
167 | row.seasonId.trim(),
168 | row.teamAbbreviation.trim(),
169 | row.playerAge.trim(),
170 | row.gp.trim(),
171 | row.min.trim(),
172 | row.pts.trim(),
173 | row.fgPct.trim(),
174 | row.fg3Pct.trim(),
175 | row.ftPct.trim(),
176 | row.ast.trim(),
177 | row.reb.trim(),
178 | row.stl.trim(),
179 | row.blk.trim(),
180 | row.tov.trim(),
181 | ])
182 | );
183 | });
184 |
185 | const overallRow = makeRow(overallArr);
186 | seasonTable.push(
187 | alignCenter([
188 | bold('Overall'),
189 | bold(''),
190 | bold(''),
191 | bold(overallRow.gp.trim()),
192 | bold(overallRow.min.trim()),
193 | bold(overallRow.pts.trim()),
194 | bold(overallRow.fgPct.trim()),
195 | bold(overallRow.fg3Pct.trim()),
196 | bold(overallRow.ftPct.trim()),
197 | bold(overallRow.ast.trim()),
198 | bold(overallRow.reb.trim()),
199 | bold(overallRow.stl.trim()),
200 | bold(overallRow.blk.trim()),
201 | bold(overallRow.tov.trim()),
202 | ])
203 | );
204 | console.log(seasonTable.toString());
205 | };
206 |
207 | export default seasonStatsCompare;
208 |
--------------------------------------------------------------------------------
/src/data/boxscore.json:
--------------------------------------------------------------------------------
1 | {
2 | "sports_content": {
3 | "sports_meta": {
4 | "date_time": "20160928 1232",
5 | "season_meta": {
6 | "calendar_date": "20160928",
7 | "season_year": "2016",
8 | "stats_season_year": "2016",
9 | "stats_season_id": "42015",
10 | "stats_season_stage": "4",
11 | "roster_season_year": "2016",
12 | "schedule_season_year": "2016",
13 | "standings_season_year": "2016",
14 | "season_id": "12016",
15 | "display_year": "2016-17",
16 | "display_season": "Pre Season",
17 | "season_stage": "1"
18 | },
19 | "next": {
20 | "url":
21 | "http://data.nba.com/data/15m/json/cms/noseason/game/20160508/0041500234/boxscore.json"
22 | }
23 | },
24 | "game": {
25 | "id": "0041500234",
26 | "game_url": "20160508/SASOKC",
27 | "season_id": "42015",
28 | "date": "20160508",
29 | "time": "2000",
30 | "arena": "Chesapeake Energy Arena",
31 | "city": "Oklahoma City",
32 | "state": "OK",
33 | "country": "",
34 | "home_start_date": "20160508",
35 | "home_start_time": "1900",
36 | "visitor_start_date": "20160508",
37 | "visitor_start_time": "1900",
38 | "previewAvailable": "1",
39 | "recapAvailable": "1",
40 | "notebookAvailable": "0",
41 | "tnt_ot": "1",
42 | "attendance": "18203",
43 | "officials": [
44 | {
45 | "person_id": "1152",
46 | "first_name": "Dan",
47 | "last_name": "Crawford",
48 | "jersey_number": "43"
49 | },
50 | {
51 | "person_id": "1194",
52 | "first_name": "Bill",
53 | "last_name": "Spooner",
54 | "jersey_number": "22"
55 | },
56 | {
57 | "person_id": "2534",
58 | "first_name": "Zach",
59 | "last_name": "Zarba",
60 | "jersey_number": "15"
61 | }
62 | ],
63 | "ticket": {
64 | "ticket_link": ""
65 | },
66 | "broadcasters": {
67 | "radio": {
68 | "broadcaster": [
69 | {
70 | "scope": "natl",
71 | "home_visitor": "natl",
72 | "display_name": "Sirius:207"
73 | },
74 | {
75 | "scope": "local",
76 | "home_visitor": "visitor",
77 | "display_name": "WOAI 1200AM"
78 | },
79 | {
80 | "scope": "local",
81 | "home_visitor": "home",
82 | "display_name": "WWLS 98.1FM OKC / 930AM (ESP)"
83 | }
84 | ]
85 | },
86 | "tv": {
87 | "broadcaster": [
88 | {
89 | "scope": "natl",
90 | "home_visitor": "natl",
91 | "display_name": "TNT"
92 | },
93 | {
94 | "scope": "can",
95 | "home_visitor": "can",
96 | "display_name": "Sportsnet One"
97 | }
98 | ]
99 | }
100 | },
101 | "period_time": {
102 | "period_value": "4",
103 | "period_status": "Final",
104 | "game_status": "3",
105 | "game_clock": "",
106 | "total_periods": "4",
107 | "period_name": "Qtr"
108 | },
109 | "playoffs": {
110 | "round": "",
111 | "conference": "",
112 | "series": "",
113 | "gameId": "0041500234",
114 | "gameStatus": "1",
115 | "game_number": "4",
116 | "game_necessary_flag": "0",
117 | "home_seed": "",
118 | "visitor_seed": "",
119 | "home_wins": "0",
120 | "visitor_wins": "0"
121 | },
122 | "visitor": {
123 | "id": "1610612759",
124 | "team_key": "SAS",
125 | "city": "San Antonio",
126 | "abbreviation": "SAS",
127 | "nickname": "Spurs",
128 | "url_name": "spurs",
129 | "team_code": "spurs",
130 | "score": "97",
131 | "linescores": {
132 | "period": [
133 | {
134 | "period_value": "1",
135 | "period_name": "Q1",
136 | "score": "27"
137 | },
138 | {
139 | "period_value": "2",
140 | "period_name": "Q2",
141 | "score": "26"
142 | },
143 | {
144 | "period_value": "3",
145 | "period_name": "Q3",
146 | "score": "28"
147 | },
148 | {
149 | "period_value": "4",
150 | "period_name": "Q4",
151 | "score": "16"
152 | }
153 | ]
154 | },
155 | "Leaders": {
156 | "Points": {
157 | "PlayerCount": "1",
158 | "StatValue": "22",
159 | "leader": [
160 | {
161 | "PersonID": "2225",
162 | "PlayerCode": "tony_parker",
163 | "FirstName": "Tony",
164 | "LastName": "Parker"
165 | }
166 | ]
167 | },
168 | "Assists": {
169 | "PlayerCount": "2",
170 | "StatValue": "3",
171 | "leader": [
172 | {
173 | "PersonID": "2225",
174 | "PlayerCode": "tony_parker",
175 | "FirstName": "Tony",
176 | "LastName": "Parker"
177 | },
178 | {
179 | "PersonID": "201988",
180 | "PlayerCode": "patrick_mills",
181 | "FirstName": "Patty",
182 | "LastName": "Mills"
183 | }
184 | ]
185 | },
186 | "Rebounds": {
187 | "PlayerCount": "1",
188 | "StatValue": "7",
189 | "leader": [
190 | {
191 | "PersonID": "2561",
192 | "PlayerCode": "david_west",
193 | "FirstName": "David",
194 | "LastName": "West"
195 | }
196 | ]
197 | }
198 | },
199 | "stats": {
200 | "points": "97",
201 | "field_goals_made": "40",
202 | "field_goals_attempted": "85",
203 | "field_goals_percentage": "47.1",
204 | "free_throws_made": "15",
205 | "free_throws_attempted": "16",
206 | "free_throws_percentage": "93.8",
207 | "three_pointers_made": "2",
208 | "three_pointers_attempted": "12",
209 | "three_pointers_percentage": "16.7",
210 | "rebounds_offensive": "9",
211 | "rebounds_defensive": "25",
212 | "team_rebounds": "7",
213 | "assists": "12",
214 | "fouls": "21",
215 | "team_fouls": "6",
216 | "technical_fouls": "1",
217 | "steals": "11",
218 | "turnovers": "12",
219 | "team_turnovers": "2",
220 | "blocks": "2",
221 | "short_timeout_remaining": "1",
222 | "full_timeout_remaining": "1"
223 | },
224 | "players": {
225 | "player": [
226 | {
227 | "first_name": "Kawhi",
228 | "last_name": "Leonard",
229 | "jersey_number": "2",
230 | "person_id": "202695",
231 | "position_short": "SF",
232 | "position_full": "Forward",
233 | "minutes": "40",
234 | "seconds": "18",
235 | "points": "21",
236 | "field_goals_made": "7",
237 | "field_goals_attempted": "19",
238 | "player_code": "kawhi_leonard",
239 | "free_throws_made": "7",
240 | "free_throws_attempted": "7",
241 | "three_pointers_made": "0",
242 | "three_pointers_attempted": "4",
243 | "rebounds_offensive": "2",
244 | "rebounds_defensive": "4",
245 | "assists": "2",
246 | "fouls": "2",
247 | "steals": "4",
248 | "turnovers": "4",
249 | "team_turnovers": "",
250 | "blocks": "0",
251 | "plus_minus": "-19",
252 | "on_court": "0",
253 | "starting_position": "SF"
254 | },
255 | {
256 | "first_name": "LaMarcus",
257 | "last_name": "Aldridge",
258 | "jersey_number": "12",
259 | "person_id": "200746",
260 | "position_short": "PF",
261 | "position_full": "Forward",
262 | "minutes": "37",
263 | "seconds": "6",
264 | "points": "20",
265 | "field_goals_made": "8",
266 | "field_goals_attempted": "18",
267 | "player_code": "lamarcus_aldridge",
268 | "free_throws_made": "4",
269 | "free_throws_attempted": "5",
270 | "three_pointers_made": "0",
271 | "three_pointers_attempted": "0",
272 | "rebounds_offensive": "2",
273 | "rebounds_defensive": "4",
274 | "assists": "0",
275 | "fouls": "2",
276 | "steals": "0",
277 | "turnovers": "2",
278 | "team_turnovers": "",
279 | "blocks": "0",
280 | "plus_minus": "-13",
281 | "on_court": "0",
282 | "starting_position": "PF"
283 | },
284 | {
285 | "first_name": "Tim",
286 | "last_name": "Duncan",
287 | "jersey_number": "21",
288 | "person_id": "1495",
289 | "position_short": "C",
290 | "position_full": "Center",
291 | "minutes": "12",
292 | "seconds": "6",
293 | "points": "0",
294 | "field_goals_made": "0",
295 | "field_goals_attempted": "0",
296 | "player_code": "",
297 | "free_throws_made": "0",
298 | "free_throws_attempted": "0",
299 | "three_pointers_made": "0",
300 | "three_pointers_attempted": "0",
301 | "rebounds_offensive": "1",
302 | "rebounds_defensive": "2",
303 | "assists": "0",
304 | "fouls": "4",
305 | "steals": "0",
306 | "turnovers": "0",
307 | "team_turnovers": "",
308 | "blocks": "0",
309 | "plus_minus": "-5",
310 | "on_court": "0",
311 | "starting_position": "C"
312 | },
313 | {
314 | "first_name": "Danny",
315 | "last_name": "Green",
316 | "jersey_number": "14",
317 | "person_id": "201980",
318 | "position_short": "G-F",
319 | "position_full": "Guard-Forward",
320 | "minutes": "27",
321 | "seconds": "42",
322 | "points": "0",
323 | "field_goals_made": "0",
324 | "field_goals_attempted": "3",
325 | "player_code": "daniel_green",
326 | "free_throws_made": "0",
327 | "free_throws_attempted": "0",
328 | "three_pointers_made": "0",
329 | "three_pointers_attempted": "1",
330 | "rebounds_offensive": "0",
331 | "rebounds_defensive": "5",
332 | "assists": "1",
333 | "fouls": "5",
334 | "steals": "2",
335 | "turnovers": "0",
336 | "team_turnovers": "",
337 | "blocks": "0",
338 | "plus_minus": "-18",
339 | "on_court": "0",
340 | "starting_position": "SG"
341 | },
342 | {
343 | "first_name": "Tony",
344 | "last_name": "Parker",
345 | "jersey_number": "9",
346 | "person_id": "2225",
347 | "position_short": "PG",
348 | "position_full": "Guard",
349 | "minutes": "32",
350 | "seconds": "19",
351 | "points": "22",
352 | "field_goals_made": "10",
353 | "field_goals_attempted": "16",
354 | "player_code": "tony_parker",
355 | "free_throws_made": "2",
356 | "free_throws_attempted": "2",
357 | "three_pointers_made": "0",
358 | "three_pointers_attempted": "1",
359 | "rebounds_offensive": "0",
360 | "rebounds_defensive": "0",
361 | "assists": "3",
362 | "fouls": "0",
363 | "steals": "1",
364 | "turnovers": "4",
365 | "team_turnovers": "",
366 | "blocks": "1",
367 | "plus_minus": "-14",
368 | "on_court": "0",
369 | "starting_position": "PG"
370 | },
371 | {
372 | "first_name": "Manu",
373 | "last_name": "Ginobili",
374 | "jersey_number": "20",
375 | "person_id": "1938",
376 | "position_short": "G",
377 | "position_full": "Guard",
378 | "minutes": "21",
379 | "seconds": "30",
380 | "points": "9",
381 | "field_goals_made": "3",
382 | "field_goals_attempted": "5",
383 | "player_code": "emanuel_ginobili",
384 | "free_throws_made": "2",
385 | "free_throws_attempted": "2",
386 | "three_pointers_made": "1",
387 | "three_pointers_attempted": "2",
388 | "rebounds_offensive": "0",
389 | "rebounds_defensive": "2",
390 | "assists": "1",
391 | "fouls": "0",
392 | "steals": "1",
393 | "turnovers": "1",
394 | "team_turnovers": "",
395 | "blocks": "0",
396 | "plus_minus": "5",
397 | "on_court": "0",
398 | "starting_position": ""
399 | },
400 | {
401 | "first_name": "David",
402 | "last_name": "West",
403 | "jersey_number": "3",
404 | "person_id": "2561",
405 | "position_short": "F",
406 | "position_full": "Forward",
407 | "minutes": "24",
408 | "seconds": "45",
409 | "points": "8",
410 | "field_goals_made": "4",
411 | "field_goals_attempted": "10",
412 | "player_code": "david_west",
413 | "free_throws_made": "0",
414 | "free_throws_attempted": "0",
415 | "three_pointers_made": "0",
416 | "three_pointers_attempted": "1",
417 | "rebounds_offensive": "2",
418 | "rebounds_defensive": "5",
419 | "assists": "2",
420 | "fouls": "5",
421 | "steals": "0",
422 | "turnovers": "1",
423 | "team_turnovers": "",
424 | "blocks": "1",
425 | "plus_minus": "2",
426 | "on_court": "1",
427 | "starting_position": ""
428 | },
429 | {
430 | "first_name": "Patty",
431 | "last_name": "Mills",
432 | "jersey_number": "8",
433 | "person_id": "201988",
434 | "position_short": "G",
435 | "position_full": "Guard",
436 | "minutes": "15",
437 | "seconds": "41",
438 | "points": "4",
439 | "field_goals_made": "2",
440 | "field_goals_attempted": "4",
441 | "player_code": "patrick_mills",
442 | "free_throws_made": "0",
443 | "free_throws_attempted": "0",
444 | "three_pointers_made": "0",
445 | "three_pointers_attempted": "2",
446 | "rebounds_offensive": "0",
447 | "rebounds_defensive": "2",
448 | "assists": "3",
449 | "fouls": "0",
450 | "steals": "0",
451 | "turnovers": "0",
452 | "team_turnovers": "",
453 | "blocks": "0",
454 | "plus_minus": "0",
455 | "on_court": "1",
456 | "starting_position": ""
457 | },
458 | {
459 | "first_name": "Kyle",
460 | "last_name": "Anderson",
461 | "jersey_number": "1",
462 | "person_id": "203937",
463 | "position_short": "F",
464 | "position_full": "Forward",
465 | "minutes": "5",
466 | "seconds": "46",
467 | "points": "2",
468 | "field_goals_made": "1",
469 | "field_goals_attempted": "2",
470 | "player_code": "kyle_anderson",
471 | "free_throws_made": "0",
472 | "free_throws_attempted": "0",
473 | "three_pointers_made": "0",
474 | "three_pointers_attempted": "0",
475 | "rebounds_offensive": "0",
476 | "rebounds_defensive": "0",
477 | "assists": "0",
478 | "fouls": "1",
479 | "steals": "3",
480 | "turnovers": "0",
481 | "team_turnovers": "",
482 | "blocks": "0",
483 | "plus_minus": "4",
484 | "on_court": "1",
485 | "starting_position": ""
486 | },
487 | {
488 | "first_name": "Boris",
489 | "last_name": "Diaw",
490 | "jersey_number": "33",
491 | "person_id": "2564",
492 | "position_short": "C-F",
493 | "position_full": "Center-Forward",
494 | "minutes": "21",
495 | "seconds": "19",
496 | "points": "11",
497 | "field_goals_made": "5",
498 | "field_goals_attempted": "8",
499 | "player_code": "boris_diaw",
500 | "free_throws_made": "0",
501 | "free_throws_attempted": "0",
502 | "three_pointers_made": "1",
503 | "three_pointers_attempted": "1",
504 | "rebounds_offensive": "2",
505 | "rebounds_defensive": "1",
506 | "assists": "0",
507 | "fouls": "2",
508 | "steals": "0",
509 | "turnovers": "0",
510 | "team_turnovers": "",
511 | "blocks": "0",
512 | "plus_minus": "-12",
513 | "on_court": "0",
514 | "starting_position": ""
515 | },
516 | {
517 | "first_name": "Kevin",
518 | "last_name": "Martin",
519 | "jersey_number": "23",
520 | "person_id": "2755",
521 | "position_short": "G",
522 | "position_full": "Guard",
523 | "minutes": "0",
524 | "seconds": "44",
525 | "points": "0",
526 | "field_goals_made": "0",
527 | "field_goals_attempted": "0",
528 | "player_code": "kevin_martin",
529 | "free_throws_made": "0",
530 | "free_throws_attempted": "0",
531 | "three_pointers_made": "0",
532 | "three_pointers_attempted": "0",
533 | "rebounds_offensive": "0",
534 | "rebounds_defensive": "0",
535 | "assists": "0",
536 | "fouls": "0",
537 | "steals": "0",
538 | "turnovers": "0",
539 | "team_turnovers": "",
540 | "blocks": "0",
541 | "plus_minus": "0",
542 | "on_court": "1",
543 | "starting_position": ""
544 | },
545 | {
546 | "first_name": "Boban",
547 | "last_name": "Marjanovic",
548 | "jersey_number": "51",
549 | "person_id": "1626246",
550 | "position_short": "C",
551 | "position_full": "Center",
552 | "minutes": "0",
553 | "seconds": "44",
554 | "points": "0",
555 | "field_goals_made": "0",
556 | "field_goals_attempted": "0",
557 | "player_code": "boban_marjanovic",
558 | "free_throws_made": "0",
559 | "free_throws_attempted": "0",
560 | "three_pointers_made": "0",
561 | "three_pointers_attempted": "0",
562 | "rebounds_offensive": "0",
563 | "rebounds_defensive": "0",
564 | "assists": "0",
565 | "fouls": "0",
566 | "steals": "0",
567 | "turnovers": "0",
568 | "team_turnovers": "",
569 | "blocks": "0",
570 | "plus_minus": "0",
571 | "on_court": "1",
572 | "starting_position": ""
573 | }
574 | ]
575 | }
576 | },
577 | "home": {
578 | "id": "1610612760",
579 | "team_key": "OKC",
580 | "city": "Oklahoma City",
581 | "abbreviation": "OKC",
582 | "nickname": "Thunder",
583 | "url_name": "thunder",
584 | "team_code": "thunder",
585 | "score": "111",
586 | "linescores": {
587 | "period": [
588 | {
589 | "period_value": "1",
590 | "period_name": "Q1",
591 | "score": "17"
592 | },
593 | {
594 | "period_value": "2",
595 | "period_name": "Q2",
596 | "score": "28"
597 | },
598 | {
599 | "period_value": "3",
600 | "period_name": "Q3",
601 | "score": "32"
602 | },
603 | {
604 | "period_value": "4",
605 | "period_name": "Q4",
606 | "score": "34"
607 | }
608 | ]
609 | },
610 | "Leaders": {
611 | "Points": {
612 | "PlayerCount": "1",
613 | "StatValue": "41",
614 | "leader": [
615 | {
616 | "PersonID": "201142",
617 | "PlayerCode": "kevin_durant",
618 | "FirstName": "Kevin",
619 | "LastName": "Durant"
620 | }
621 | ]
622 | },
623 | "Assists": {
624 | "PlayerCount": "1",
625 | "StatValue": "15",
626 | "leader": [
627 | {
628 | "PersonID": "201566",
629 | "PlayerCode": "russell_westbrook",
630 | "FirstName": "Russell",
631 | "LastName": "Westbrook"
632 | }
633 | ]
634 | },
635 | "Rebounds": {
636 | "PlayerCount": "1",
637 | "StatValue": "11",
638 | "leader": [
639 | {
640 | "PersonID": "203500",
641 | "PlayerCode": "steven_adams",
642 | "FirstName": "Steven",
643 | "LastName": "Adams"
644 | }
645 | ]
646 | }
647 | },
648 | "stats": {
649 | "points": "111",
650 | "field_goals_made": "40",
651 | "field_goals_attempted": "79",
652 | "field_goals_percentage": "50.6",
653 | "free_throws_made": "22",
654 | "free_throws_attempted": "29",
655 | "free_throws_percentage": "75.9",
656 | "three_pointers_made": "9",
657 | "three_pointers_attempted": "23",
658 | "three_pointers_percentage": "39.1",
659 | "rebounds_offensive": "10",
660 | "rebounds_defensive": "30",
661 | "team_rebounds": "11",
662 | "assists": "23",
663 | "fouls": "18",
664 | "team_fouls": "5",
665 | "technical_fouls": "1",
666 | "steals": "6",
667 | "turnovers": "12",
668 | "team_turnovers": "2",
669 | "blocks": "3",
670 | "short_timeout_remaining": "2",
671 | "full_timeout_remaining": "1"
672 | },
673 | "players": {
674 | "player": [
675 | {
676 | "first_name": "Kevin",
677 | "last_name": "Durant",
678 | "jersey_number": "35",
679 | "person_id": "201142",
680 | "position_short": "F",
681 | "position_full": "Forward",
682 | "minutes": "43",
683 | "seconds": "23",
684 | "points": "41",
685 | "field_goals_made": "14",
686 | "field_goals_attempted": "25",
687 | "player_code": "kevin_durant",
688 | "free_throws_made": "10",
689 | "free_throws_attempted": "13",
690 | "three_pointers_made": "3",
691 | "three_pointers_attempted": "9",
692 | "rebounds_offensive": "1",
693 | "rebounds_defensive": "4",
694 | "assists": "4",
695 | "fouls": "1",
696 | "steals": "1",
697 | "turnovers": "5",
698 | "team_turnovers": "",
699 | "blocks": "0",
700 | "plus_minus": "17",
701 | "on_court": "0",
702 | "starting_position": "SF"
703 | },
704 | {
705 | "first_name": "Serge",
706 | "last_name": "Ibaka",
707 | "jersey_number": "7",
708 | "person_id": "201586",
709 | "position_short": "F",
710 | "position_full": "Forward",
711 | "minutes": "28",
712 | "seconds": "6",
713 | "points": "7",
714 | "field_goals_made": "3",
715 | "field_goals_attempted": "6",
716 | "player_code": "serge_ibaka",
717 | "free_throws_made": "0",
718 | "free_throws_attempted": "0",
719 | "three_pointers_made": "1",
720 | "three_pointers_attempted": "3",
721 | "rebounds_offensive": "0",
722 | "rebounds_defensive": "1",
723 | "assists": "0",
724 | "fouls": "1",
725 | "steals": "0",
726 | "turnovers": "0",
727 | "team_turnovers": "",
728 | "blocks": "1",
729 | "plus_minus": "4",
730 | "on_court": "0",
731 | "starting_position": "PF"
732 | },
733 | {
734 | "first_name": "Steven",
735 | "last_name": "Adams",
736 | "jersey_number": "12",
737 | "person_id": "203500",
738 | "position_short": "C",
739 | "position_full": "Center",
740 | "minutes": "36",
741 | "seconds": "5",
742 | "points": "16",
743 | "field_goals_made": "6",
744 | "field_goals_attempted": "8",
745 | "player_code": "steven_adams",
746 | "free_throws_made": "4",
747 | "free_throws_attempted": "6",
748 | "three_pointers_made": "0",
749 | "three_pointers_attempted": "1",
750 | "rebounds_offensive": "2",
751 | "rebounds_defensive": "9",
752 | "assists": "0",
753 | "fouls": "5",
754 | "steals": "0",
755 | "turnovers": "1",
756 | "team_turnovers": "",
757 | "blocks": "2",
758 | "plus_minus": "21",
759 | "on_court": "1",
760 | "starting_position": "C"
761 | },
762 | {
763 | "first_name": "Andre",
764 | "last_name": "Roberson",
765 | "jersey_number": "21",
766 | "person_id": "203460",
767 | "position_short": "G-F",
768 | "position_full": "Guard-Forward",
769 | "minutes": "20",
770 | "seconds": "16",
771 | "points": "0",
772 | "field_goals_made": "0",
773 | "field_goals_attempted": "2",
774 | "player_code": "andre_roberson",
775 | "free_throws_made": "0",
776 | "free_throws_attempted": "0",
777 | "three_pointers_made": "0",
778 | "three_pointers_attempted": "1",
779 | "rebounds_offensive": "1",
780 | "rebounds_defensive": "3",
781 | "assists": "1",
782 | "fouls": "1",
783 | "steals": "1",
784 | "turnovers": "1",
785 | "team_turnovers": "",
786 | "blocks": "0",
787 | "plus_minus": "-4",
788 | "on_court": "0",
789 | "starting_position": "SG"
790 | },
791 | {
792 | "first_name": "Russell",
793 | "last_name": "Westbrook",
794 | "jersey_number": "0",
795 | "person_id": "201566",
796 | "position_short": "PG",
797 | "position_full": "Guard",
798 | "minutes": "37",
799 | "seconds": "33",
800 | "points": "14",
801 | "field_goals_made": "5",
802 | "field_goals_attempted": "18",
803 | "player_code": "russell_westbrook",
804 | "free_throws_made": "3",
805 | "free_throws_attempted": "4",
806 | "three_pointers_made": "1",
807 | "three_pointers_attempted": "3",
808 | "rebounds_offensive": "3",
809 | "rebounds_defensive": "4",
810 | "assists": "15",
811 | "fouls": "2",
812 | "steals": "3",
813 | "turnovers": "3",
814 | "team_turnovers": "",
815 | "blocks": "0",
816 | "plus_minus": "18",
817 | "on_court": "0",
818 | "starting_position": "PG"
819 | },
820 | {
821 | "first_name": "Dion",
822 | "last_name": "Waiters",
823 | "jersey_number": "11",
824 | "person_id": "203079",
825 | "position_short": "G",
826 | "position_full": "Guard",
827 | "minutes": "29",
828 | "seconds": "20",
829 | "points": "17",
830 | "field_goals_made": "7",
831 | "field_goals_attempted": "11",
832 | "player_code": "dion_waiters",
833 | "free_throws_made": "1",
834 | "free_throws_attempted": "2",
835 | "three_pointers_made": "2",
836 | "three_pointers_attempted": "2",
837 | "rebounds_offensive": "0",
838 | "rebounds_defensive": "3",
839 | "assists": "3",
840 | "fouls": "3",
841 | "steals": "1",
842 | "turnovers": "0",
843 | "team_turnovers": "",
844 | "blocks": "0",
845 | "plus_minus": "6",
846 | "on_court": "1",
847 | "starting_position": ""
848 | },
849 | {
850 | "first_name": "Enes",
851 | "last_name": "Kanter",
852 | "jersey_number": "11",
853 | "person_id": "202683",
854 | "position_short": "C",
855 | "position_full": "Center",
856 | "minutes": "28",
857 | "seconds": "19",
858 | "points": "11",
859 | "field_goals_made": "3",
860 | "field_goals_attempted": "6",
861 | "player_code": "enes_kanter",
862 | "free_throws_made": "4",
863 | "free_throws_attempted": "4",
864 | "three_pointers_made": "1",
865 | "three_pointers_attempted": "2",
866 | "rebounds_offensive": "3",
867 | "rebounds_defensive": "5",
868 | "assists": "0",
869 | "fouls": "3",
870 | "steals": "0",
871 | "turnovers": "0",
872 | "team_turnovers": "",
873 | "blocks": "0",
874 | "plus_minus": "9",
875 | "on_court": "1",
876 | "starting_position": ""
877 | },
878 | {
879 | "first_name": "Nick",
880 | "last_name": "Collison",
881 | "jersey_number": "4",
882 | "person_id": "2555",
883 | "position_short": "F",
884 | "position_full": "Forward",
885 | "minutes": "3",
886 | "seconds": "30",
887 | "points": "0",
888 | "field_goals_made": "0",
889 | "field_goals_attempted": "0",
890 | "player_code": "nick_collison",
891 | "free_throws_made": "0",
892 | "free_throws_attempted": "0",
893 | "three_pointers_made": "0",
894 | "three_pointers_attempted": "0",
895 | "rebounds_offensive": "0",
896 | "rebounds_defensive": "1",
897 | "assists": "0",
898 | "fouls": "1",
899 | "steals": "0",
900 | "turnovers": "0",
901 | "team_turnovers": "",
902 | "blocks": "0",
903 | "plus_minus": "-6",
904 | "on_court": "0",
905 | "starting_position": ""
906 | },
907 | {
908 | "first_name": "Cameron",
909 | "last_name": "Payne",
910 | "jersey_number": "22",
911 | "person_id": "1626166",
912 | "position_short": "G",
913 | "position_full": "Guard",
914 | "minutes": "4",
915 | "seconds": "3",
916 | "points": "0",
917 | "field_goals_made": "0",
918 | "field_goals_attempted": "0",
919 | "player_code": "cameron_payne",
920 | "free_throws_made": "0",
921 | "free_throws_attempted": "0",
922 | "three_pointers_made": "0",
923 | "three_pointers_attempted": "0",
924 | "rebounds_offensive": "0",
925 | "rebounds_defensive": "0",
926 | "assists": "0",
927 | "fouls": "0",
928 | "steals": "0",
929 | "turnovers": "2",
930 | "team_turnovers": "",
931 | "blocks": "0",
932 | "plus_minus": "-1",
933 | "on_court": "1",
934 | "starting_position": ""
935 | },
936 | {
937 | "first_name": "Randy",
938 | "last_name": "Foye",
939 | "jersey_number": "2",
940 | "person_id": "200751",
941 | "position_short": "G",
942 | "position_full": "Guard",
943 | "minutes": "9",
944 | "seconds": "25",
945 | "points": "5",
946 | "field_goals_made": "2",
947 | "field_goals_attempted": "3",
948 | "player_code": "randy_foye",
949 | "free_throws_made": "0",
950 | "free_throws_attempted": "0",
951 | "three_pointers_made": "1",
952 | "three_pointers_attempted": "2",
953 | "rebounds_offensive": "0",
954 | "rebounds_defensive": "0",
955 | "assists": "0",
956 | "fouls": "1",
957 | "steals": "0",
958 | "turnovers": "0",
959 | "team_turnovers": "",
960 | "blocks": "0",
961 | "plus_minus": "6",
962 | "on_court": "1",
963 | "starting_position": ""
964 | }
965 | ]
966 | }
967 | },
968 | "lp": {
969 | "lp_video": "true",
970 | "condensed_bb": "",
971 | "visitor": {
972 | "audio": {
973 | "ENG": "false",
974 | "SPA": "false"
975 | },
976 | "video": {
977 | "avl": "false",
978 | "onAir": "false",
979 | "archBB": "false"
980 | }
981 | },
982 | "home": {
983 | "audio": {
984 | "ENG": "false",
985 | "SPA": "false"
986 | },
987 | "video": {
988 | "avl": "false",
989 | "onAir": "false",
990 | "archBB": "false"
991 | }
992 | }
993 | },
994 | "dl": {
995 | "link": []
996 | }
997 | }
998 | }
999 | }
1000 |
--------------------------------------------------------------------------------
/src/data/scoreboard.json:
--------------------------------------------------------------------------------
1 | {
2 | "sports_content": {
3 | "sports_meta": {
4 | "date_time": "20160930 1444",
5 | "season_meta": {
6 | "calendar_date": "20160930",
7 | "season_year": "2016",
8 | "stats_season_year": "2016",
9 | "stats_season_id": "42015",
10 | "stats_season_stage": "4",
11 | "roster_season_year": "2016",
12 | "schedule_season_year": "2016",
13 | "standings_season_year": "2016",
14 | "season_id": "12016",
15 | "display_year": "2016-17",
16 | "display_season": "Pre Season",
17 | "season_stage": "1"
18 | },
19 | "next": {
20 | "url":
21 | "http://data.nba.com/data/5s/json/cms/noseason/scoreboard/20160503/games.json"
22 | }
23 | },
24 | "games": {
25 | "game": [
26 | {
27 | "id": "0041500211",
28 | "game_url": "20160503/MIATOR",
29 | "season_id": "42015",
30 | "date": "20160503",
31 | "time": "2000",
32 | "arena": "Air Canada Centre",
33 | "city": "Toronto",
34 | "state": "ON",
35 | "country": "",
36 | "home_start_date": "20160503",
37 | "home_start_time": "2000",
38 | "visitor_start_date": "20160503",
39 | "visitor_start_time": "2000",
40 | "previewAvailable": "1",
41 | "recapAvailable": "1",
42 | "notebookAvailable": "0",
43 | "tnt_ot": "1",
44 | "buzzerBeater": "0",
45 | "ticket": {
46 | "ticket_link": ""
47 | },
48 | "period_time": {
49 | "period_value": "5",
50 | "period_status": "Final",
51 | "game_status": "3",
52 | "game_clock": "",
53 | "total_periods": "4",
54 | "period_name": "Qtr"
55 | },
56 | "lp": {
57 | "lp_video": "false",
58 | "condensed_bb": "false",
59 | "visitor": {
60 | "audio": {
61 | "ENG": "false",
62 | "SPA": "false"
63 | },
64 | "video": {
65 | "avl": "false",
66 | "onAir": "false",
67 | "archBB": "false"
68 | }
69 | },
70 | "home": {
71 | "audio": {
72 | "ENG": "false",
73 | "SPA": "false"
74 | },
75 | "video": {
76 | "avl": "false",
77 | "onAir": "false",
78 | "archBB": "false"
79 | }
80 | }
81 | },
82 | "dl": {
83 | "link": [
84 | {
85 | "name": "TNT",
86 | "long_nm": "",
87 | "code": "tnt",
88 | "url": "http://www.tntdrama.com/watchtnt/",
89 | "mobile_url": "",
90 | "home_visitor": "natl"
91 | },
92 | {
93 | "name": "TNTOT",
94 | "long_nm": "",
95 | "code": "tntot",
96 | "url": "http://www.nba.com/tntovertime/",
97 | "mobile_url": "",
98 | "home_visitor": "natl"
99 | }
100 | ]
101 | },
102 | "broadcasters": {
103 | "radio": {
104 | "broadcaster": [
105 | {
106 | "scope": "natl",
107 | "home_visitor": "natl",
108 | "display_name": "Sirius:207"
109 | },
110 | {
111 | "scope": "local",
112 | "home_visitor": "visitor",
113 | "display_name": "790 The Ticket / S: WRTO MIX 98.3 FM"
114 | },
115 | {
116 | "scope": "local",
117 | "home_visitor": "home",
118 | "display_name": "TSN Radio 1050 Toronto"
119 | }
120 | ]
121 | },
122 | "tv": {
123 | "broadcaster": [
124 | {
125 | "scope": "natl",
126 | "home_visitor": "natl",
127 | "display_name": "TNT"
128 | },
129 | {
130 | "scope": "local",
131 | "home_visitor": "home",
132 | "display_name": "TSN"
133 | }
134 | ]
135 | }
136 | },
137 | "playoffs": {
138 | "round": "",
139 | "conference": "",
140 | "series": "",
141 | "gameId": "0041500211",
142 | "gameStatus": "1",
143 | "game_number": "1",
144 | "game_necessary_flag": "0",
145 | "home_seed": "",
146 | "visitor_seed": "",
147 | "home_wins": "0",
148 | "visitor_wins": "0"
149 | },
150 | "visitor": {
151 | "id": "1610612748",
152 | "team_key": "MIA",
153 | "city": "Miami",
154 | "abbreviation": "MIA",
155 | "nickname": "Heat",
156 | "url_name": "heat",
157 | "team_code": "heat",
158 | "score": "102",
159 | "linescores": {
160 | "period": [
161 | {
162 | "period_value": "1",
163 | "period_name": "Q1",
164 | "score": "18"
165 | },
166 | {
167 | "period_value": "2",
168 | "period_name": "Q2",
169 | "score": "23"
170 | },
171 | {
172 | "period_value": "3",
173 | "period_name": "Q3",
174 | "score": "27"
175 | },
176 | {
177 | "period_value": "4",
178 | "period_name": "Q4",
179 | "score": "22"
180 | },
181 | {
182 | "period_value": "5",
183 | "period_name": "OT1",
184 | "score": "12"
185 | }
186 | ]
187 | }
188 | },
189 | "home": {
190 | "id": "1610612761",
191 | "team_key": "TOR",
192 | "city": "Toronto",
193 | "abbreviation": "TOR",
194 | "nickname": "Raptors",
195 | "url_name": "raptors",
196 | "team_code": "raptors",
197 | "score": "96",
198 | "linescores": {
199 | "period": [
200 | {
201 | "period_value": "1",
202 | "period_name": "Q1",
203 | "score": "18"
204 | },
205 | {
206 | "period_value": "2",
207 | "period_name": "Q2",
208 | "score": "25"
209 | },
210 | {
211 | "period_value": "3",
212 | "period_name": "Q3",
213 | "score": "20"
214 | },
215 | {
216 | "period_value": "4",
217 | "period_name": "Q4",
218 | "score": "27"
219 | },
220 | {
221 | "period_value": "5",
222 | "period_name": "OT1",
223 | "score": "6"
224 | }
225 | ]
226 | }
227 | }
228 | },
229 | {
230 | "id": "0041500222",
231 | "game_url": "20160503/PORGSW",
232 | "season_id": "42015",
233 | "date": "20160503",
234 | "time": "2230",
235 | "arena": "ORACLE Arena",
236 | "city": "Oakland",
237 | "state": "CA",
238 | "country": "",
239 | "home_start_date": "20160503",
240 | "home_start_time": "1930",
241 | "visitor_start_date": "20160503",
242 | "visitor_start_time": "1930",
243 | "previewAvailable": "1",
244 | "recapAvailable": "1",
245 | "notebookAvailable": "0",
246 | "tnt_ot": "1",
247 | "buzzerBeater": "0",
248 | "ticket": {
249 | "ticket_link": ""
250 | },
251 | "period_time": {
252 | "period_value": "4",
253 | "period_status": "Final",
254 | "game_status": "3",
255 | "game_clock": "",
256 | "total_periods": "4",
257 | "period_name": "Qtr"
258 | },
259 | "lp": {
260 | "lp_video": "false",
261 | "condensed_bb": "false",
262 | "visitor": {
263 | "audio": {
264 | "ENG": "false",
265 | "SPA": "false"
266 | },
267 | "video": {
268 | "avl": "false",
269 | "onAir": "false",
270 | "archBB": "false"
271 | }
272 | },
273 | "home": {
274 | "audio": {
275 | "ENG": "false",
276 | "SPA": "false"
277 | },
278 | "video": {
279 | "avl": "false",
280 | "onAir": "false",
281 | "archBB": "false"
282 | }
283 | }
284 | },
285 | "dl": {
286 | "link": [
287 | {
288 | "name": "TNT",
289 | "long_nm": "",
290 | "code": "tnt",
291 | "url": "http://www.tntdrama.com/watchtnt/",
292 | "mobile_url": "",
293 | "home_visitor": "natl"
294 | },
295 | {
296 | "name": "TNTOT",
297 | "long_nm": "",
298 | "code": "tntot",
299 | "url": "http://www.nba.com/tntovertime/",
300 | "mobile_url": "",
301 | "home_visitor": "natl"
302 | }
303 | ]
304 | },
305 | "broadcasters": {
306 | "radio": {
307 | "broadcaster": [
308 | {
309 | "scope": "natl",
310 | "home_visitor": "natl",
311 | "display_name": "Sirius:207"
312 | },
313 | {
314 | "scope": "local",
315 | "home_visitor": "visitor",
316 | "display_name": "Rip City Radio 620"
317 | },
318 | {
319 | "scope": "local",
320 | "home_visitor": "home",
321 | "display_name": "KGO 810AM/KTCT 1050AM"
322 | }
323 | ]
324 | },
325 | "tv": {
326 | "broadcaster": [
327 | {
328 | "scope": "natl",
329 | "home_visitor": "natl",
330 | "display_name": "TNT"
331 | },
332 | {
333 | "scope": "can",
334 | "home_visitor": "can",
335 | "display_name": "TSN3/5"
336 | }
337 | ]
338 | }
339 | },
340 | "playoffs": {
341 | "round": "",
342 | "conference": "",
343 | "series": "",
344 | "gameId": "0041500222",
345 | "gameStatus": "1",
346 | "game_number": "2",
347 | "game_necessary_flag": "0",
348 | "home_seed": "",
349 | "visitor_seed": "",
350 | "home_wins": "0",
351 | "visitor_wins": "0"
352 | },
353 | "visitor": {
354 | "id": "1610612757",
355 | "team_key": "POR",
356 | "city": "Portland",
357 | "abbreviation": "POR",
358 | "nickname": "Trail Blazers",
359 | "url_name": "blazers",
360 | "team_code": "blazers",
361 | "score": "99",
362 | "linescores": {
363 | "period": [
364 | {
365 | "period_value": "1",
366 | "period_name": "Q1",
367 | "score": "34"
368 | },
369 | {
370 | "period_value": "2",
371 | "period_name": "Q2",
372 | "score": "25"
373 | },
374 | {
375 | "period_value": "3",
376 | "period_name": "Q3",
377 | "score": "28"
378 | },
379 | {
380 | "period_value": "4",
381 | "period_name": "Q4",
382 | "score": "12"
383 | }
384 | ]
385 | }
386 | },
387 | "home": {
388 | "id": "1610612744",
389 | "team_key": "GSW",
390 | "city": "Golden State",
391 | "abbreviation": "GSW",
392 | "nickname": "Warriors",
393 | "url_name": "warriors",
394 | "team_code": "warriors",
395 | "score": "110",
396 | "linescores": {
397 | "period": [
398 | {
399 | "period_value": "1",
400 | "period_name": "Q1",
401 | "score": "21"
402 | },
403 | {
404 | "period_value": "2",
405 | "period_name": "Q2",
406 | "score": "30"
407 | },
408 | {
409 | "period_value": "3",
410 | "period_name": "Q3",
411 | "score": "25"
412 | },
413 | {
414 | "period_value": "4",
415 | "period_name": "Q4",
416 | "score": "34"
417 | }
418 | ]
419 | }
420 | }
421 | }
422 | ]
423 | }
424 | }
425 | }
426 |
--------------------------------------------------------------------------------
/src/utils/__tests__/blessed.spec.js:
--------------------------------------------------------------------------------
1 | import getBlessed from '../blessed';
2 |
3 | jest.mock('blessed');
4 |
5 | const blessed = require('blessed');
6 |
7 | const mockTeam = () => ({
8 | getFullName: jest.fn(() => 'Golden State Warriors'),
9 | getWins: jest.fn(() => '82'),
10 | getLoses: jest.fn(() => '82'),
11 | });
12 |
13 | describe('getBlessed', () => {
14 | beforeEach(() => {
15 | blessed.screen.mockReturnValue({
16 | append: jest.fn(),
17 | key: jest.fn((keyArr, cb) => {
18 | process.stdin.on('keypress', (ch, key) => {
19 | if (key && keyArr.indexOf(key.name) > -1) {
20 | cb();
21 | }
22 | });
23 | }),
24 | });
25 | });
26 |
27 | afterEach(() => {
28 | jest.resetAllMocks();
29 | });
30 |
31 | it('should exist', () => {
32 | expect(getBlessed).toBeDefined();
33 | });
34 |
35 | describe('screen', () => {
36 | it('should call blessed.screen once', () => {
37 | getBlessed(mockTeam(), mockTeam());
38 |
39 | expect(blessed.screen).toBeCalledWith({
40 | smartCSR: true,
41 | fullUnicode: true,
42 | title: 'NBA-GO',
43 | });
44 | expect(blessed.screen.mock.calls.length).toBe(1);
45 | });
46 |
47 | it('should append 15 components and 1 key event', () => {
48 | const { screen } = getBlessed(mockTeam(), mockTeam());
49 |
50 | expect(screen.append.mock.calls.length).toBe(15);
51 | expect(screen.key.mock.calls.length).toBe(1);
52 | });
53 | });
54 |
55 | describe('others', () => {
56 | it('should call blessed.box twice, blessed.table twice, blessed.text 9 times and blessed.bigtext twice', () => {
57 | getBlessed(mockTeam(), mockTeam());
58 |
59 | expect(blessed.box.mock.calls.length).toBe(2);
60 | expect(blessed.table.mock.calls.length).toBe(2);
61 | expect(blessed.text.mock.calls.length).toBe(9);
62 | expect(blessed.bigtext.mock.calls.length).toBe(2);
63 | });
64 |
65 | it('should call process.exit when press esc', () => {
66 | process.exit = jest.fn();
67 | getBlessed(mockTeam(), mockTeam());
68 |
69 | process.stdin.emit('keypress', '', { name: 'escape' });
70 | expect(process.exit).toBeCalledWith(1);
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/src/utils/__tests__/catchAPIError.spec.js:
--------------------------------------------------------------------------------
1 | import catchAPIError from '../catchAPIError';
2 | import { error } from '../log';
3 |
4 | jest.mock('../log');
5 | process.exit = jest.fn();
6 |
7 | describe('catchAPIError', () => {
8 | it('should exist', () => {
9 | expect(catchAPIError).toBeDefined();
10 | });
11 |
12 | it('should work', () => {
13 | catchAPIError('error message', 'NBA.getGames()');
14 |
15 | expect(error).toBeCalledWith('error message');
16 | expect(process.exit).toBeCalledWith(1);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/utils/__tests__/cfonts.spec.js:
--------------------------------------------------------------------------------
1 | import { cfontsDate } from '../cfonts';
2 |
3 | jest.mock('cfonts');
4 |
5 | const CFonts = require('cfonts');
6 |
7 | describe('cfonts', () => {
8 | it('should call Cfonts.say and format date', () => {
9 | cfontsDate('2017-11-11');
10 |
11 | expect(CFonts.say).toBeCalledWith('2017/11/11', {
12 | font: 'block',
13 | align: 'left',
14 | colors: ['blue', 'red'],
15 | background: 'black',
16 | letterSpacing: 1,
17 | lineHeight: 1,
18 | space: true,
19 | maxLength: '10',
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/utils/__tests__/convertUnit.spec.js:
--------------------------------------------------------------------------------
1 | import { convertToCm, convertToKg } from '../convertUnit';
2 |
3 | describe('convertUnit', () => {
4 | describe('convertToCm', () => {
5 | it('should convert foot-inches to cm', () => {
6 | const result = convertToCm('6-11');
7 |
8 | expect(result).toBe('210.82');
9 | expect(typeof result).toBe('string');
10 | });
11 |
12 | it('should return empty string when pass empty string to it', () => {
13 | const result = convertToCm('');
14 |
15 | expect(result).toBe('');
16 | expect(typeof result).toBe('string');
17 | });
18 | });
19 |
20 | describe('convertToKg', () => {
21 | it('should convert pounds to kg', () => {
22 | const result = convertToKg(100);
23 |
24 | expect(result).toBe('45.36');
25 | expect(typeof result).toBe('string');
26 | });
27 |
28 | it('should return empty string when pass empty string to it', () => {
29 | const result = convertToKg('');
30 |
31 | expect(result).toBe('');
32 | expect(typeof result).toBe('string');
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/utils/__tests__/getApiDate.spec.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment-timezone';
2 | import getTime from 'date-fns/get_time';
3 |
4 | import getApiDate from '../getApiDate';
5 |
6 | const getDateByCity = city =>
7 | moment()
8 | .tz(city)
9 | .format();
10 |
11 | describe('getApiDate', () => {
12 | // 1524281047645 = Sat Apr 21 2018 11:23:45 GMT+0800 (CST)
13 | // 1524196800000 = 2018-04-20T00:00:00.000-04:00
14 |
15 | jest.spyOn(Date, 'now').mockImplementation(() => 1524281047645);
16 |
17 | it('should exist', () => {
18 | expect(getApiDate).toBeDefined();
19 | });
20 |
21 | it('should work fine in GMT+08:00', () => {
22 | const date = getDateByCity('Asia/Taipei');
23 |
24 | expect(date).toBe('2018-04-21T11:24:07+08:00');
25 | expect(getApiDate(getTime(date))).toEqual({
26 | day: 20,
27 | month: 4,
28 | year: 2018,
29 | });
30 | });
31 |
32 | it('should work fine in GMT+01:00', () => {
33 | const date = getDateByCity('Europe/London');
34 |
35 | expect(date).toBe('2018-04-21T04:24:07+01:00');
36 | expect(getApiDate(getTime(date))).toEqual({
37 | day: 20,
38 | month: 4,
39 | year: 2018,
40 | });
41 | });
42 |
43 | it('should work fine in GMT-07:00', () => {
44 | const date = getDateByCity('America/Los_Angeles');
45 |
46 | expect(date).toBe('2018-04-20T20:24:07-07:00');
47 | expect(getApiDate(getTime(date))).toEqual({
48 | day: 20,
49 | month: 4,
50 | year: 2018,
51 | });
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/utils/__tests__/log.spec.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 |
3 | import { error, bold, nbaRed, neonGreen, colorTeamName } from '../log';
4 |
5 | const _log = console.log;
6 |
7 | describe('console', () => {
8 | beforeEach(() => {
9 | console.log = jest.fn();
10 | });
11 | afterEach(() => {
12 | console.log = _log;
13 | });
14 |
15 | it('error', () => {
16 | error('error!');
17 | expect(console.log).toBeCalledWith(chalk`{red.bold error!}`);
18 | });
19 |
20 | it('bold', () => {
21 | expect(bold('bold!')).toEqual(chalk`{white.bold bold!}`);
22 | });
23 |
24 | it('nbaRed', () => {
25 | expect(nbaRed('nbaRed!')).toEqual(chalk`{bold.hex('#f00b47') nbaRed!}`);
26 | });
27 |
28 | it('neonGreen', () => {
29 | expect(neonGreen('neonGreen!')).toEqual(chalk`{hex('#66ff66') neonGreen!}`);
30 | });
31 |
32 | it('colorTeamName', () => {
33 | expect(colorTeamName('#123456', 'Cool')).toEqual(
34 | chalk`{bold.white.bgHex('123456') Cool}`
35 | );
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/utils/__tests__/nba.spec.js:
--------------------------------------------------------------------------------
1 | import NBA from 'nba';
2 | import { getGames, getBoxScore, getPlayByPlay } from 'nba-stats-client';
3 |
4 | import nba from '../nba';
5 |
6 | jest.mock('nba');
7 | jest.mock('nba-stats-client');
8 |
9 | describe('NBA', () => {
10 | it('updatePlayers should work', async () => {
11 | await nba.updatePlayers();
12 |
13 | expect(NBA.updatePlayers).toBeCalled();
14 | });
15 |
16 | it('searchPlayers should work', async () => {
17 | await nba.searchPlayers();
18 |
19 | expect(NBA.searchPlayers).toBeCalled();
20 | });
21 |
22 | it('playerInfo should work', async () => {
23 | await nba.playerInfo();
24 |
25 | expect(NBA.stats.playerInfo).toBeCalled();
26 | });
27 |
28 | it('playerProfile should work', async () => {
29 | await nba.playerProfile();
30 |
31 | expect(NBA.stats.playerProfile).toBeCalled();
32 | });
33 |
34 | it('teamSplits should work', async () => {
35 | await nba.teamSplits();
36 | expect(NBA.stats.teamSplits).toBeCalled();
37 | });
38 |
39 | it('teamInfoCommon should work', async () => {
40 | await nba.teamInfoCommon();
41 |
42 | expect(NBA.stats.teamInfoCommon).toBeCalled();
43 | });
44 |
45 | it('getPlayByPlay should work', async () => {
46 | await nba.getPlayByPlay();
47 |
48 | expect(getPlayByPlay).toBeCalled();
49 | });
50 |
51 | it('getBoxScore should work', async () => {
52 | await nba.getBoxScore();
53 |
54 | expect(getBoxScore).toBeCalled();
55 | });
56 |
57 | it('getGames should work', async () => {
58 | await nba.getGames();
59 |
60 | expect(getGames).toBeCalled();
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/src/utils/__tests__/table.spec.js:
--------------------------------------------------------------------------------
1 | import Table from 'cli-table3';
2 |
3 | import { basicTable } from '../table';
4 |
5 | describe('table', () => {
6 | it('should exist', () => {
7 | expect(basicTable).toBeDefined();
8 | });
9 |
10 | it('should return a Table instance', () => {
11 | const table = basicTable();
12 |
13 | expect(table).toBeInstanceOf(Table);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/utils/blessed.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import blessed from 'blessed';
3 | import { right } from 'wide-align';
4 |
5 | const getBlessed = (homeTeam, visitorTeam) => {
6 | const screen = blessed.screen({
7 | smartCSR: true,
8 | fullUnicode: true,
9 | title: 'NBA-GO',
10 | });
11 |
12 | const baseBox = blessed.box({
13 | top: 0,
14 | left: 0,
15 | width: '100%',
16 | height: '100%',
17 | padding: 0,
18 | style: {
19 | fg: 'black',
20 | bg: 'black',
21 | border: {
22 | fg: '#f0f0f0',
23 | bg: 'black',
24 | },
25 | },
26 | });
27 |
28 | const scoreboardTable = blessed.table({
29 | top: 5,
30 | left: 'center',
31 | width: '33%',
32 | height: 8,
33 | tags: true,
34 | border: {
35 | type: 'line',
36 | },
37 | style: {
38 | header: {
39 | fg: 'white',
40 | },
41 | cell: {
42 | fg: 'white',
43 | },
44 | },
45 | });
46 |
47 | const homeTeamFullNameText = blessed.text({
48 | parent: screen,
49 | top: 7,
50 | left: `33%-${homeTeam.getFullName({ color: false }).length + 24}`,
51 | width: 25,
52 | align: 'left',
53 | content: `${homeTeam.getFullName({
54 | color: true,
55 | })}`,
56 | style: {
57 | fg: 'white',
58 | },
59 | });
60 |
61 | const homeTeamStandingsText = blessed.text({
62 | top: 8,
63 | left: '33%-39',
64 | width: 15,
65 | align: 'right',
66 | content: right(`HOME (${homeTeam.getWins()} - ${homeTeam.getLoses()})`, 15),
67 | style: {
68 | fg: '#fbfbfb',
69 | },
70 | });
71 |
72 | const homeTeamScoreText = blessed.bigtext({
73 | font: path.join(__dirname, './fonts/ter-u12n.json'),
74 | fontBold: path.join(__dirname, './fonts/ter-u12b.json'),
75 | top: 2,
76 | left: '33%-20',
77 | width: 15,
78 | align: 'right',
79 | vlign: 'center',
80 | style: {
81 | fg: 'white',
82 | },
83 | });
84 |
85 | const visitorTeamFullNameText = blessed.text({
86 | top: 7,
87 | left: '66%+28',
88 | width: 25,
89 | align: 'left',
90 | content: `${visitorTeam.getFullName({
91 | color: true,
92 | })}`,
93 | tags: true,
94 | style: {
95 | fg: 'white',
96 | },
97 | });
98 |
99 | const visitorTeamStandingsText = blessed.text({
100 | top: 8,
101 | left: '66%+28',
102 | width: 15,
103 | align: 'left',
104 | content: `(${visitorTeam.getWins()} - ${visitorTeam.getLoses()}) AWAY`,
105 | style: {
106 | fg: '#fbfbfb',
107 | },
108 | });
109 |
110 | const visitorTeamScoreText = blessed.bigtext({
111 | font: path.join(__dirname, './fonts/ter-u12n.json'),
112 | fontBold: path.join(__dirname, './fonts/ter-u12b.json'),
113 | top: 2,
114 | left: '66%+6',
115 | width: 15,
116 | align: 'left',
117 | style: {
118 | fg: 'white',
119 | },
120 | });
121 |
122 | const seasonText = blessed.text({
123 | top: 0,
124 | left: 'center',
125 | align: 'center',
126 | style: {
127 | fg: 'white',
128 | },
129 | });
130 |
131 | const timeText = blessed.text({
132 | top: 13,
133 | left: 'center',
134 | align: 'center',
135 | style: {
136 | fg: 'white',
137 | },
138 | });
139 |
140 | const dateText = blessed.text({
141 | top: 2,
142 | left: 'center',
143 | align: 'center',
144 | style: {
145 | fg: 'white',
146 | },
147 | });
148 |
149 | const arenaText = blessed.text({
150 | top: 3,
151 | left: 'center',
152 | align: 'center',
153 | style: {
154 | fg: 'white',
155 | },
156 | });
157 |
158 | const networkText = blessed.text({
159 | top: 4,
160 | left: 'center',
161 | align: 'center',
162 | style: {
163 | fg: 'white',
164 | },
165 | });
166 |
167 | const playByPlayBox = blessed.box({
168 | parent: screen,
169 | top: 15,
170 | left: 3,
171 | width: '70%-3',
172 | height: '100%-15',
173 | padding: {
174 | top: 0,
175 | right: 0,
176 | left: 2,
177 | bottom: 0,
178 | },
179 | align: 'left',
180 | keys: true,
181 | mouse: false,
182 | scrollable: true,
183 | focused: true,
184 | label: ' Play By Play ',
185 | border: {
186 | type: 'line',
187 | },
188 | scrollbar: {
189 | ch: ' ',
190 | track: {
191 | bg: '#0253a4',
192 | },
193 | style: {
194 | inverse: true,
195 | },
196 | },
197 | });
198 |
199 | const boxscoreTable = blessed.table({
200 | parent: screen,
201 | top: 15,
202 | left: '70%',
203 | width: '30%-3',
204 | height: '100%-15',
205 | tags: true,
206 | pad: 0,
207 | label: ' Box Score ',
208 | border: {
209 | type: 'line',
210 | },
211 | });
212 |
213 | screen.append(baseBox);
214 | screen.append(seasonText);
215 | screen.append(timeText);
216 | screen.append(dateText);
217 | screen.append(arenaText);
218 | screen.append(networkText);
219 | screen.append(homeTeamFullNameText);
220 | screen.append(homeTeamStandingsText);
221 | screen.append(homeTeamScoreText);
222 | screen.append(visitorTeamFullNameText);
223 | screen.append(visitorTeamStandingsText);
224 | screen.append(visitorTeamScoreText);
225 | screen.append(scoreboardTable);
226 | screen.append(playByPlayBox);
227 | screen.append(boxscoreTable);
228 | screen.key(['escape', 'q', 'C-c'], () => process.exit(1));
229 |
230 | return {
231 | screen,
232 | scoreboardTable,
233 | seasonText,
234 | timeText,
235 | dateText,
236 | arenaText,
237 | networkText,
238 | homeTeamScoreText,
239 | visitorTeamScoreText,
240 | playByPlayBox,
241 | boxscoreTable,
242 | };
243 | };
244 |
245 | export default getBlessed;
246 |
--------------------------------------------------------------------------------
/src/utils/catchAPIError.js:
--------------------------------------------------------------------------------
1 | import { error } from './log';
2 |
3 | const catchAPIError = (err, apiName) => {
4 | console.log('');
5 | console.log('');
6 | error(err);
7 | console.log('');
8 | error(`Oops, ${apiName} goes wrong.`);
9 | error(
10 | 'Please run nba-go again.\nIf it still does not work, feel free to open an issue on https://github.com/xxhomey19/nba-go/issues'
11 | );
12 |
13 | process.exit(1);
14 | };
15 |
16 | export default catchAPIError;
17 |
--------------------------------------------------------------------------------
/src/utils/cfonts.js:
--------------------------------------------------------------------------------
1 | import CFonts from 'cfonts';
2 | import format from 'date-fns/format';
3 |
4 | export const cfontsDate = date => {
5 | CFonts.say(format(date, 'YYYY/MM/DD'), {
6 | font: 'block',
7 | align: 'left',
8 | colors: ['blue', 'red'],
9 | background: 'black',
10 | letterSpacing: 1,
11 | lineHeight: 1,
12 | space: true,
13 | maxLength: '10',
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/utils/convertUnit.js:
--------------------------------------------------------------------------------
1 | const convertToCm = length => {
2 | if (length !== '') {
3 | const [foot, inches] = length.split('-');
4 |
5 | return (foot * 30.48 + inches * 2.54).toFixed(2);
6 | }
7 |
8 | return '';
9 | };
10 |
11 | const convertToKg = weight =>
12 | weight !== '' ? (weight * 0.45359237).toFixed(2) : '';
13 |
14 | export { convertToCm, convertToKg };
15 |
--------------------------------------------------------------------------------
/src/utils/getApiDate.js:
--------------------------------------------------------------------------------
1 | import startOfDay from 'date-fns/start_of_day';
2 | import { DateTime } from 'luxon';
3 |
4 | export default function(date) {
5 | const targetDate = DateTime.fromJSDate(startOfDay(date), {
6 | zone: 'America/New_York',
7 | }).startOf('day');
8 |
9 | return {
10 | year: targetDate.year,
11 | month: targetDate.month,
12 | day: targetDate.day,
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/log.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 |
3 | export const error = msg => {
4 | console.log(chalk`{red.bold ${msg}}`);
5 | };
6 |
7 | export const bold = msg => chalk`{white.bold ${msg}}`;
8 |
9 | export const nbaRed = msg => chalk`{bold.hex('#f00b47') ${msg}}`;
10 |
11 | export const neonGreen = msg => chalk`{hex('#66ff66') ${msg}}`;
12 |
13 | export const colorTeamName = (color, name) =>
14 | chalk`{bold.white.bgHex('${color}') ${name}}`;
15 |
--------------------------------------------------------------------------------
/src/utils/nba.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import NBA from 'nba';
3 | import { getGames, getBoxScore, getPlayByPlay } from 'nba-stats-client';
4 |
5 | const nbaClient = {
6 | getGames,
7 | getBoxScore,
8 | getPlayByPlay,
9 | };
10 |
11 | const essentialMethods = [
12 | 'updatePlayers',
13 | 'searchPlayers',
14 | 'playerInfo',
15 | 'playerProfile',
16 | 'teamSplits',
17 | 'teamInfoCommon',
18 | 'getGames',
19 | 'getBoxScore',
20 | 'getPlayByPlay',
21 | ];
22 |
23 | const pickEssentialMethods = obj => R.pick(essentialMethods, obj);
24 |
25 | export default R.compose(
26 | R.mergeAll,
27 | R.map(pickEssentialMethods)
28 | )([R.omit(['stats'], NBA), R.prop('stats', NBA), nbaClient]);
29 |
--------------------------------------------------------------------------------
/src/utils/setSeason.js:
--------------------------------------------------------------------------------
1 | import R from 'ramda';
2 | import parse from 'date-fns/parse';
3 | import getMonth from 'date-fns/get_month';
4 | import getYear from 'date-fns/get_year';
5 | import emoji from 'node-emoji';
6 |
7 | import { error } from './log';
8 |
9 | const setSeason = date => {
10 | const year = R.compose(
11 | getYear,
12 | parse
13 | )(date);
14 | const month = R.compose(
15 | getMonth,
16 | parse
17 | )(date);
18 |
19 | if (year < 2012 || (year === 2012 && month < 5)) {
20 | error(
21 | `Sorry, https://stats.nba.com/ doesn't provide season data before 2012-13 ${emoji.get(
22 | 'confused'
23 | )}`
24 | );
25 |
26 | process.exit(1);
27 | }
28 |
29 | if (month > 9) {
30 | process.env.season = `${year}-${(year + 1).toString().slice(-2)}`;
31 | } else {
32 | process.env.season = `${year - 1}-${year.toString().slice(-2)}`;
33 | }
34 |
35 | return date;
36 | };
37 |
38 | export default setSeason;
39 |
--------------------------------------------------------------------------------
/src/utils/table.js:
--------------------------------------------------------------------------------
1 | import Table from 'cli-table3';
2 |
3 | export const basicTable = () =>
4 | new Table({
5 | head: [],
6 | chars: {
7 | top: '═',
8 | 'top-mid': '╤',
9 | 'top-left': '╔',
10 | 'top-right': '╗',
11 | bottom: '═',
12 | 'bottom-mid': '╧',
13 | 'bottom-left': '╚',
14 | 'bottom-right': '╝',
15 | left: '║',
16 | 'left-mid': '╟',
17 | mid: '─',
18 | 'mid-mid': '┼',
19 | right: '║',
20 | 'right-mid': '╢',
21 | middle: '│',
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const CopyWebpackPlugin = require('copy-webpack-plugin');
4 | const TerserPlugin = require('terser-webpack-plugin');
5 | const nodeExternals = require('webpack-node-externals');
6 |
7 | module.exports = {
8 | target: 'node',
9 | node: {
10 | __dirname: false,
11 | },
12 | externals: [nodeExternals()],
13 | entry: {
14 | cli: path.join(__dirname, 'src', 'cli.js'),
15 | },
16 | output: {
17 | path: path.join(__dirname, 'lib'),
18 | filename: '[name].js',
19 | },
20 | module: {
21 | rules: [
22 | {
23 | test: /\.js$/,
24 | loader: 'babel-loader',
25 | exclude: path.resolve(__dirname, 'node_modules'),
26 | },
27 | ],
28 | },
29 | plugins: [
30 | new webpack.DefinePlugin({
31 | 'process.env.NODE_ENV': JSON.stringify('production'),
32 | }),
33 | new webpack.LoaderOptionsPlugin({
34 | minimize: true,
35 | debug: false,
36 | }),
37 | new CopyWebpackPlugin([
38 | {
39 | from: path.join(__dirname, 'src', 'utils', 'fonts'),
40 | to: path.join(__dirname, 'lib', 'fonts'),
41 | },
42 | ]),
43 | ],
44 | optimization: {
45 | minimizer: [new TerserPlugin()],
46 | },
47 | };
48 |
--------------------------------------------------------------------------------