playball 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/cli.js +1 -1
- package/dist/components/App.js +12 -3
- package/dist/components/GameList.js +7 -11
- package/dist/components/Standings.js +43 -1
- package/dist/config.js +5 -0
- package/dist/features/schedule.js +26 -3
- package/dist/features/standings.js +13 -3
- package/dist/main.js +2 -1
- package/dist/utils.js +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,32 @@ key | action
|
|
|
69
69
|
----|--------
|
|
70
70
|
<kbd>↓</kbd>/<kbd>j</kbd>, <kbd>↑</kbd>/<kbd>k</kbd> | scroll list of all plays
|
|
71
71
|
|
|
72
|
+
### World Baseball Classic
|
|
73
|
+
|
|
74
|
+
Watch World Baseball Classic games by setting the sport configuration:
|
|
75
|
+
|
|
76
|
+
```shell
|
|
77
|
+
playball config sport wbc
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or use an environment variable for one-time viewing:
|
|
81
|
+
|
|
82
|
+
```shell
|
|
83
|
+
PLAYBALL_SPORT=wbc playball
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To switch back to MLB:
|
|
87
|
+
|
|
88
|
+
```shell
|
|
89
|
+
playball config sport mlb
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
When running via Docker:
|
|
93
|
+
|
|
94
|
+
```shell
|
|
95
|
+
docker run -it --rm -e PLAYBALL_SPORT=wbc paaatrick0/playball
|
|
96
|
+
```
|
|
97
|
+
|
|
72
98
|
### Configuration
|
|
73
99
|
|
|
74
100
|
Playball can be configured using the `config` subcommand. To list the current configuration values run the subcommand with no additional arguments:
|
|
@@ -132,6 +158,7 @@ key | description | default | allowed values
|
|
|
132
158
|
`favorites` | Teams to highlight in schedule and standings views | | Any one of the following: `ATL`, `AZ`, `BAL`, `BOS`, `CHC`, `CIN`, `CLE`, `COL`, `CWS`, `DET`, `HOU`, `KC`, `LAA`, `LAD`, `MIA`, `MIL`, `MIN`, `NYM`, `NYY`, `OAK`, `PHI`, `PIT`, `SD`, `SEA`, `SF`, `STL`, `TB`, `TEX`, `TOR`, `WSH`. Or a comma-separated list of multiple (e.g. `SEA,MIL`).<br/><br />Note: in some terminals the list must be quoted: `playball config favorites "SEA,MIL"`
|
|
133
159
|
`title` | If enabled, the terminal title will be set to the score of the current game | `false` | `false`, `true`
|
|
134
160
|
`live-delay` | Number of seconds to delay the live game stream. Useful when watching with delayed broadcast streams. | `0` (no delay) | Any positive number
|
|
161
|
+
`sport` | Which sport/league to display | `mlb` | `mlb`, `wbc`
|
|
135
162
|
|
|
136
163
|
### Development
|
|
137
164
|
```
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ const notifier = updateNotifier({
|
|
|
10
10
|
updateCheckInterval: 1000 * 60 * 60 * 24 * 7 // 1 week
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
program.name(pkg.name).description(pkg.description).version(pkg.version).option('--replay <gameId>', 'Replay a game by game ID').action(main).hook('postAction', () => notifier.notify({
|
|
13
|
+
program.name(pkg.name).description(pkg.description).version(pkg.version).option('--replay <gameId>', 'Replay a game by game ID').option('--date <date>', 'Open schedule to specific date (YYYY-MM-DD)').action(main).hook('postAction', () => notifier.notify({
|
|
14
14
|
isGlobal: true
|
|
15
15
|
}));
|
|
16
16
|
program.command('config').description('Set or get configration values').argument('[key]').argument('[value]').option('--unset', 'Unset configuration value').action((key, value, options) => {
|
package/dist/components/App.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { useDispatch } from "react-redux/lib/alternate-renderers.js";
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
|
+
import { parseISO } from 'date-fns';
|
|
4
5
|
import GameList from "./GameList.js";
|
|
5
6
|
import HelpBar from "./HelpBar.js";
|
|
6
|
-
import {
|
|
7
|
+
import { setLiveGame, setReplayGame } from "../features/games.js";
|
|
7
8
|
import Game from "./Game.js";
|
|
8
9
|
import useKey from "../hooks/useKey.js";
|
|
9
10
|
import Standings from "./Standings.js";
|
|
11
|
+
import { setDate } from "../features/schedule.js";
|
|
10
12
|
const SCHEDULE = 'schedule';
|
|
11
13
|
const STANDINGS = 'standings';
|
|
12
14
|
const GAME = 'game';
|
|
13
15
|
function App({
|
|
14
|
-
replayId
|
|
16
|
+
replayId,
|
|
17
|
+
defaultDate
|
|
15
18
|
}) {
|
|
16
19
|
const [view, setView] = useState(SCHEDULE);
|
|
17
20
|
const dispatch = useDispatch();
|
|
@@ -35,6 +38,11 @@ function App({
|
|
|
35
38
|
dispatch(setReplayGame(replayId)).unwrap().then(() => setView(GAME)).catch(() => setView(SCHEDULE));
|
|
36
39
|
}
|
|
37
40
|
}, [replayId]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (defaultDate) {
|
|
43
|
+
dispatch(setDate(parseISO(defaultDate)));
|
|
44
|
+
}
|
|
45
|
+
}, [defaultDate]);
|
|
38
46
|
return /*#__PURE__*/React.createElement("element", null, /*#__PURE__*/React.createElement("element", {
|
|
39
47
|
top: 0,
|
|
40
48
|
left: 0,
|
|
@@ -48,6 +56,7 @@ function App({
|
|
|
48
56
|
}, /*#__PURE__*/React.createElement(HelpBar, null)));
|
|
49
57
|
}
|
|
50
58
|
App.propTypes = {
|
|
51
|
-
replayId: PropTypes.string
|
|
59
|
+
replayId: PropTypes.string,
|
|
60
|
+
defaultDate: PropTypes.string
|
|
52
61
|
};
|
|
53
62
|
export default App;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
2
|
-
import React, { useCallback, useEffect, useRef
|
|
2
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
3
3
|
import { useDispatch, useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
|
-
import {
|
|
6
|
-
import { fetchSchedule, selectData, selectLoading } from "../features/schedule.js";
|
|
5
|
+
import { format } from 'date-fns';
|
|
6
|
+
import { fetchSchedule, nextDay, prevDay, selectData, selectLoading, selectScheduleDate, setDate } from "../features/schedule.js";
|
|
7
7
|
import { teamFavoriteStar } from "../utils.js";
|
|
8
8
|
import Grid from "./Grid.js";
|
|
9
9
|
import useKey from "../hooks/useKey.js";
|
|
@@ -98,8 +98,8 @@ function GameList({
|
|
|
98
98
|
const dispatch = useDispatch();
|
|
99
99
|
const schedule = useSelector(selectData);
|
|
100
100
|
const loading = useSelector(selectLoading);
|
|
101
|
+
const date = useSelector(selectScheduleDate);
|
|
101
102
|
const timerRef = useRef(null);
|
|
102
|
-
const [date, setDate] = useState(new Date());
|
|
103
103
|
let games = [];
|
|
104
104
|
if (schedule && schedule.dates.length > 0) {
|
|
105
105
|
games = schedule.dates[0].games.slice().sort(compareGames);
|
|
@@ -109,19 +109,15 @@ function GameList({
|
|
|
109
109
|
timerRef.current = setInterval(() => dispatch(fetchSchedule(date)), 30000);
|
|
110
110
|
return () => clearInterval(timerRef.current);
|
|
111
111
|
}, [date]);
|
|
112
|
-
useKey('p', useCallback(() =>
|
|
113
|
-
days: -1
|
|
114
|
-
})), []), {
|
|
112
|
+
useKey('p', useCallback(() => dispatch(prevDay()), []), {
|
|
115
113
|
key: 'P',
|
|
116
114
|
label: 'Prev Day'
|
|
117
115
|
});
|
|
118
|
-
useKey('n', useCallback(() =>
|
|
119
|
-
days: 1
|
|
120
|
-
})), []), {
|
|
116
|
+
useKey('n', useCallback(() => dispatch(nextDay()), []), {
|
|
121
117
|
key: 'N',
|
|
122
118
|
label: 'Next Day'
|
|
123
119
|
});
|
|
124
|
-
useKey('t', useCallback(() => setDate(new Date()), []), {
|
|
120
|
+
useKey('t', useCallback(() => dispatch(setDate(new Date())), []), {
|
|
125
121
|
key: 'T',
|
|
126
122
|
label: 'Today'
|
|
127
123
|
});
|
|
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { useDispatch, useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
4
4
|
import { fetchStandings, selectData } from "../features/standings.js";
|
|
5
|
-
import { teamFavoriteStar } from "../utils.js";
|
|
5
|
+
import { teamFavoriteStar, getSport } from "../utils.js";
|
|
6
6
|
function formatHeaderRow(record) {
|
|
7
7
|
return record.division.nameShort.padEnd(15) + ' W' + ' L' + ' PCT' + ' GB' + ' WCGB' + ' L10' + ' STRK';
|
|
8
8
|
}
|
|
@@ -49,10 +49,52 @@ Division.propTypes = {
|
|
|
49
49
|
function Standings() {
|
|
50
50
|
const dispatch = useDispatch();
|
|
51
51
|
const standings = useSelector(selectData);
|
|
52
|
+
const sport = getSport();
|
|
52
53
|
useEffect(() => dispatch(fetchStandings()), []);
|
|
53
54
|
if (!standings) {
|
|
54
55
|
return /*#__PURE__*/React.createElement("element", null);
|
|
55
56
|
}
|
|
57
|
+
if (sport === 'wbc') {
|
|
58
|
+
// WBC Pool Standings
|
|
59
|
+
if (!standings.records || standings.records.length === 0) {
|
|
60
|
+
return /*#__PURE__*/React.createElement("box", {
|
|
61
|
+
top: 0,
|
|
62
|
+
left: 0,
|
|
63
|
+
width: "100%",
|
|
64
|
+
height: "100%"
|
|
65
|
+
}, /*#__PURE__*/React.createElement("text", {
|
|
66
|
+
top: 1,
|
|
67
|
+
left: 2
|
|
68
|
+
}, "WBC Standings not available during this phase of the tournament."), /*#__PURE__*/React.createElement("text", {
|
|
69
|
+
top: 3,
|
|
70
|
+
left: 2
|
|
71
|
+
}, "Use Schedule view to see games and results."));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Render WBC pools (records likely grouped by pool/division)
|
|
75
|
+
const halfPoint = Math.ceil(standings.records.length / 2);
|
|
76
|
+
return /*#__PURE__*/React.createElement("element", null, standings.records.slice(0, halfPoint).map((pool, idx) => {
|
|
77
|
+
var _pool$division;
|
|
78
|
+
return /*#__PURE__*/React.createElement(Division, {
|
|
79
|
+
top: idx * 7,
|
|
80
|
+
left: 0,
|
|
81
|
+
width: "50%-1",
|
|
82
|
+
key: ((_pool$division = pool.division) === null || _pool$division === void 0 ? void 0 : _pool$division.id) || idx,
|
|
83
|
+
record: pool
|
|
84
|
+
});
|
|
85
|
+
}), standings.records.slice(halfPoint).map((pool, idx) => {
|
|
86
|
+
var _pool$division2;
|
|
87
|
+
return /*#__PURE__*/React.createElement(Division, {
|
|
88
|
+
top: idx * 7,
|
|
89
|
+
left: "50%+1",
|
|
90
|
+
width: "50%-1",
|
|
91
|
+
key: ((_pool$division2 = pool.division) === null || _pool$division2 === void 0 ? void 0 : _pool$division2.id) || idx + halfPoint,
|
|
92
|
+
record: pool
|
|
93
|
+
});
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// MLB AL/NL Standings (existing logic)
|
|
56
98
|
return /*#__PURE__*/React.createElement("element", null, standings.records.filter(record => record.league.id === 103).map((record, idx) => /*#__PURE__*/React.createElement(Division, {
|
|
57
99
|
top: idx * 7,
|
|
58
100
|
left: 0,
|
package/dist/config.js
CHANGED
|
@@ -5,21 +5,38 @@ const {
|
|
|
5
5
|
createSlice,
|
|
6
6
|
createSelector
|
|
7
7
|
} = reduxjsToolkit;
|
|
8
|
-
import { format } from 'date-fns';
|
|
8
|
+
import { add, format } from 'date-fns';
|
|
9
|
+
import { getSportId } from "../utils.js";
|
|
9
10
|
const initialState = {
|
|
11
|
+
scheduleDate: new Date(),
|
|
10
12
|
loading: false,
|
|
11
13
|
error: null,
|
|
12
14
|
data: null
|
|
13
15
|
};
|
|
14
16
|
export const fetchSchedule = createAsyncThunk('schedule/fetch', async date => {
|
|
15
17
|
const dateStr = format(date, 'MM/dd/yyyy');
|
|
16
|
-
const
|
|
18
|
+
const sportId = getSportId();
|
|
19
|
+
const response = await axios.get(`http://statsapi.mlb.com/api/v1/schedule?sportId=${sportId}&hydrate=team,linescore&date=${dateStr}`);
|
|
17
20
|
return response.data;
|
|
18
21
|
});
|
|
19
22
|
export const scheduleSlice = createSlice({
|
|
20
23
|
name: 'schedule',
|
|
21
24
|
initialState,
|
|
22
|
-
reducers: {
|
|
25
|
+
reducers: {
|
|
26
|
+
nextDay(state) {
|
|
27
|
+
state.scheduleDate = add(state.scheduleDate, {
|
|
28
|
+
days: 1
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
prevDay(state) {
|
|
32
|
+
state.scheduleDate = add(state.scheduleDate, {
|
|
33
|
+
days: -1
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
setDate(state, action) {
|
|
37
|
+
state.scheduleDate = action.payload;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
23
40
|
extraReducers: builder => {
|
|
24
41
|
builder.addCase(fetchSchedule.pending, state => {
|
|
25
42
|
state.loading = true;
|
|
@@ -40,4 +57,10 @@ const scheduleSelector = state => state.schedule;
|
|
|
40
57
|
export const selectLoading = createSelector(scheduleSelector, schedule => schedule.loading);
|
|
41
58
|
export const selectError = createSelector(scheduleSelector, schedule => schedule.error);
|
|
42
59
|
export const selectData = createSelector(scheduleSelector, schedule => schedule.data);
|
|
60
|
+
export const selectScheduleDate = createSelector(scheduleSelector, schedule => schedule.scheduleDate);
|
|
61
|
+
export const {
|
|
62
|
+
nextDay,
|
|
63
|
+
prevDay,
|
|
64
|
+
setDate
|
|
65
|
+
} = scheduleSlice.actions;
|
|
43
66
|
export default scheduleSlice.reducer;
|
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
createSlice,
|
|
6
6
|
createSelector
|
|
7
7
|
} = reduxjsToolkit;
|
|
8
|
+
import { getSport } from "../utils.js";
|
|
8
9
|
const initialState = {
|
|
9
10
|
loading: false,
|
|
10
11
|
error: null,
|
|
@@ -12,9 +13,18 @@ const initialState = {
|
|
|
12
13
|
};
|
|
13
14
|
const SEASON = new Date().getFullYear();
|
|
14
15
|
export const fetchStandings = createAsyncThunk('standings/fetch', async () => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const sport = getSport();
|
|
17
|
+
if (sport === 'wbc') {
|
|
18
|
+
// WBC standings - try sportId-based endpoint
|
|
19
|
+
// If no data, return empty structure (component will handle gracefully)
|
|
20
|
+
const response = await axios.get(`https://statsapi.mlb.com/api/v1/standings?sportId=51&season=${SEASON}`);
|
|
21
|
+
return response.data;
|
|
22
|
+
} else {
|
|
23
|
+
// MLB standings (existing logic)
|
|
24
|
+
const url = `https://statsapi.mlb.com/api/v1/standings?leagueId=103,104&season=${SEASON}&standingsTypes=regularSeason&hydrate=division,team`;
|
|
25
|
+
const response = await axios.get(url);
|
|
26
|
+
return response.data;
|
|
27
|
+
}
|
|
18
28
|
});
|
|
19
29
|
export const standingsSlice = createSlice({
|
|
20
30
|
name: 'standings',
|
package/dist/main.js
CHANGED
|
@@ -17,6 +17,7 @@ export default async function startInterface(options) {
|
|
|
17
17
|
reactBlessed.render( /*#__PURE__*/React.createElement(Provider, {
|
|
18
18
|
store: store
|
|
19
19
|
}, /*#__PURE__*/React.createElement(App, {
|
|
20
|
-
replayId: options.replay
|
|
20
|
+
replayId: options.replay,
|
|
21
|
+
defaultDate: options.date
|
|
21
22
|
})), screen());
|
|
22
23
|
}
|
package/dist/utils.js
CHANGED
|
@@ -6,4 +6,26 @@ export function teamFavoriteStar(team) {
|
|
|
6
6
|
return `{${style}}★{/${style}} `;
|
|
7
7
|
}
|
|
8
8
|
return '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get the sport ID for API calls
|
|
13
|
+
* @returns {string} '51' for WBC, '1' for MLB
|
|
14
|
+
*/
|
|
15
|
+
export function getSportId() {
|
|
16
|
+
var _process$env$PLAYBALL;
|
|
17
|
+
// ENV override takes precedence
|
|
18
|
+
const envSport = (_process$env$PLAYBALL = process.env.PLAYBALL_SPORT) === null || _process$env$PLAYBALL === void 0 ? void 0 : _process$env$PLAYBALL.toLowerCase();
|
|
19
|
+
const sport = envSport || get('sport') || 'mlb';
|
|
20
|
+
return sport === 'wbc' ? '51' : '1';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the current sport setting
|
|
25
|
+
* @returns {string} 'mlb' or 'wbc'
|
|
26
|
+
*/
|
|
27
|
+
export function getSport() {
|
|
28
|
+
var _process$env$PLAYBALL2;
|
|
29
|
+
const envSport = (_process$env$PLAYBALL2 = process.env.PLAYBALL_SPORT) === null || _process$env$PLAYBALL2 === void 0 ? void 0 : _process$env$PLAYBALL2.toLowerCase();
|
|
30
|
+
return envSport || get('sport') || 'mlb';
|
|
9
31
|
}
|