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 CHANGED
@@ -69,6 +69,32 @@ key | action
69
69
  ----|--------
70
70
  <kbd>&darr;</kbd>/<kbd>j</kbd>, <kbd>&uarr;</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) => {
@@ -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 { setReplayGame, setLiveGame } from "../features/games.js";
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, useState } from 'react';
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 { add, format } from 'date-fns';
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(() => setDate(prev => add(prev, {
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(() => setDate(prev => add(prev, {
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
@@ -84,6 +84,11 @@ const schema = {
84
84
  type: 'number',
85
85
  default: 0,
86
86
  minimum: 0
87
+ },
88
+ 'sport': {
89
+ type: 'string',
90
+ enum: ['mlb', 'wbc'],
91
+ default: 'mlb'
87
92
  }
88
93
  };
89
94
  const config = new Conf({
@@ -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 response = await axios.get(`http://statsapi.mlb.com/api/v1/schedule?sportId=1&hydrate=team,linescore&date=${dateStr}`);
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 url = `https://statsapi.mlb.com/api/v1/standings?leagueId=103,104&season=${SEASON}&standingsTypes=regularSeason&hydrate=division,team`;
16
- const response = await axios.get(url);
17
- return response.data;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playball",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Watch MLB games from the comfort of your terminal",
5
5
  "keywords": [
6
6
  "MLB",