├── .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) | screen shot 2017-11-06 at 8 57 02 am | 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) | screen shot 2017-11-06 at 8 56 50 am | 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) | screen shot 2017-11-06 at 8 56 14 am | 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 | ![game -d gif](https://user-images.githubusercontent.com/12113222/32413795-0e7d75c2-c254-11e7-8a77-eeabed3c11f2.gif) 86 | 87 | ##### `-y` or `--yesterday` 88 | 89 | Check **yesterday's** schedule. 90 | 91 | ``` 92 | $ nba-go game -y 93 | ``` 94 | 95 | ![game -y gif](https://user-images.githubusercontent.com/12113222/32414094-8bd4ba98-c25a-11e7-84f0-4fc473dc7144.gif) 96 | 97 | ##### `-t` or `--today` 98 | 99 | Check **today's** schedule. 100 | 101 | ``` 102 | $ nba-go game -t 103 | ``` 104 | 105 | ![game -t gif](https://user-images.githubusercontent.com/12113222/32414115-f1a1ad72-c25a-11e7-8c79-a8b9b1ee0599.gif) 106 | 107 | ##### `-T` or `--tomorrow` 108 | 109 | Check **tomorrow's** schedule. 110 | 111 | ``` 112 | $ nba-go game -T 113 | ``` 114 | 115 | ![game -T gif](https://user-images.githubusercontent.com/12113222/32414142-7897dfe0-c25b-11e7-9acf-d50ade5379fd.gif) 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 | ![pregame](https://user-images.githubusercontent.com/12113222/32414253-ad64df82-c25d-11e7-9076-4da800f3c701.gif) 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 | ![live](https://user-images.githubusercontent.com/12113222/32420915-3ca6b34a-c2cd-11e7-904d-bf41cc4b93f7.gif) 139 | 140 | #### Final 141 | 142 | ⭐️⭐️ 143 | Check two teams' detailed scoreboard and box score. 144 | 145 | ![final](https://user-images.githubusercontent.com/12113222/32436783-1e7ad7b8-c320-11e7-97af-29d95732581c.gif) 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 | ![player -i gif](https://user-images.githubusercontent.com/12113222/32416941-7cfc49e6-c28c-11e7-8a79-15601a44554e.gif) 178 | 179 | ##### `-r` or `--regular` 180 | 181 | Get player's basic information. 182 | 183 | ``` 184 | $ nba-go player Curry -r 185 | ``` 186 | 187 | ![player -r gif](https://user-images.githubusercontent.com/12113222/32416897-bb82af9e-c28b-11e7-827f-0f0d67d80364.gif) 188 | 189 | ##### `-p` or `--playoffs` 190 | 191 | Get player's basic information. 192 | 193 | ``` 194 | $ nba-go player Curry -p 195 | ``` 196 | 197 | ![player -p gif](https://user-images.githubusercontent.com/12113222/32500032-234e8fba-c40f-11e7-87c0-6e42a66a52dc.gif) 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 | ![player -c gif](https://user-images.githubusercontent.com/12113222/37696809-1fd54306-2d14-11e8-9261-4d9b6a08588a.gif) 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 | ![player mixed gif](https://user-images.githubusercontent.com/12113222/32416928-5054d48a-c28c-11e7-84d3-bc17681e1a5e.gif) 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 | --------------------------------------------------------------------------------