sportsing 0.1.2 → 0.2.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.
Files changed (43) hide show
  1. package/README.md +160 -12
  2. package/package.json +6 -2
  3. package/src/alerts.ts +47 -0
  4. package/src/api.ts +1 -1
  5. package/src/click-to-watch.ts +39 -0
  6. package/src/commands/_lib.ts +28 -12
  7. package/src/commands/analyze.ts +2 -2
  8. package/src/commands/ask.ts +1 -1
  9. package/src/commands/cross.ts +354 -0
  10. package/src/commands/fav.ts +19 -16
  11. package/src/commands/league-ai.ts +201 -0
  12. package/src/commands/league-bracket.ts +150 -0
  13. package/src/commands/league.ts +839 -0
  14. package/src/commands/live.ts +57 -42
  15. package/src/commands/me.ts +1 -1
  16. package/src/commands/predict.ts +2 -2
  17. package/src/commands/scorers.ts +1 -1
  18. package/src/commands/setup.ts +3 -3
  19. package/src/commands/subscriptions.ts +66 -0
  20. package/src/commands/table.ts +1 -1
  21. package/src/commands/watch.ts +19 -11
  22. package/src/config.ts +166 -28
  23. package/src/espn.ts +606 -33
  24. package/src/format.ts +87 -0
  25. package/src/game-events.ts +239 -0
  26. package/src/game.ts +51 -0
  27. package/src/index.ts +35 -16
  28. package/src/league-ai.ts +359 -0
  29. package/src/league-alerts.ts +139 -0
  30. package/src/league-detect.ts +104 -0
  31. package/src/league-overlay.ts +351 -0
  32. package/src/league-panels.ts +159 -0
  33. package/src/overlay.ts +96 -51
  34. package/src/playoff-bracket.ts +384 -0
  35. package/src/recap.ts +37 -8
  36. package/src/season.ts +182 -0
  37. package/src/sports/fifa.ts +9 -4
  38. package/src/sports/nba.ts +20 -0
  39. package/src/sports/nhl.ts +20 -0
  40. package/src/standings.ts +130 -0
  41. package/src/stream.ts +49 -8
  42. package/src/watch-route.ts +95 -0
  43. package/src/watchability.ts +183 -0
@@ -0,0 +1,150 @@
1
+ // `sportsing <nba|nhl> bracket [--season YYYY]` — the playoff bracket. Once
2
+ // the postseason (or the NBA play-in) is under way, the real bracket with
3
+ // series scores; before that, a bracket projected from the standings, labelled
4
+ // as projected. `--season` shows a past postseason. The data fetching lives
5
+ // here; grouping, projection and rendering are pure in src/playoff-bracket.ts.
6
+
7
+ import { c } from "../ansi.ts";
8
+ import { getFavorites } from "../config.ts";
9
+ import {
10
+ getLeagueSeason,
11
+ getStandings,
12
+ getTeamPlayoffGames,
13
+ getTeams,
14
+ SEASON_TYPES,
15
+ type EspnTeam,
16
+ type PlayoffGame,
17
+ } from "../espn.ts";
18
+ import {
19
+ BRACKET_FORMATS,
20
+ buildSeries,
21
+ parseSeasonArg,
22
+ projectNba,
23
+ projectNhl,
24
+ renderBracket,
25
+ renderProjection,
26
+ seasonLabel,
27
+ type BracketFormat,
28
+ } from "../playoff-bracket.ts";
29
+ import { loadStandingsView } from "../standings.ts";
30
+ import { getFlag } from "./_lib.ts";
31
+ import type { LeagueConfig } from "./league.ts";
32
+
33
+ /** Resolves a league's favorites to team ids (league.ts's `favoriteIds`). */
34
+ export type FavoriteIdsFn = (cfg: LeagueConfig, teams: EspnTeam[], favs: string[]) => Set<string>;
35
+
36
+ /** ESPN season types during which the current season's bracket exists:
37
+ * postseason, off-season (it's finished), play-in. */
38
+ const BRACKET_PHASES = new Set([3, 4, 5]);
39
+
40
+ /** Schedules fetched at once — a bracket reads every team's postseason. */
41
+ const CONCURRENCY = 8;
42
+
43
+ async function mapLimit<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {
44
+ const out: R[] = new Array(items.length);
45
+ let next = 0;
46
+ const worker = async () => {
47
+ while (next < items.length) {
48
+ const i = next++;
49
+ out[i] = await fn(items[i]!);
50
+ }
51
+ };
52
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
53
+ return out;
54
+ }
55
+
56
+ /** Every team's postseason (and play-in, if the league has one) games for an
57
+ * ESPN season year. ESPN only exposes these per team; duplicates (each game
58
+ * appears on both teams' schedules) are collapsed downstream. */
59
+ async function loadPostseason(cfg: LeagueConfig, fmt: BracketFormat, teams: EspnTeam[], season: number): Promise<PlayoffGame[]> {
60
+ const types = fmt.playIn ? [SEASON_TYPES.postseason, SEASON_TYPES.playIn] : [SEASON_TYPES.postseason];
61
+ const jobs = teams.flatMap((t) => types.map((type) => ({ id: t.id, type })));
62
+ const lists = await mapLimit(jobs, CONCURRENCY, (j) => getTeamPlayoffGames(cfg.league, j.id, j.type, season));
63
+ const seen = new Set<string>();
64
+ return lists.flat().filter((pg) => !seen.has(pg.game.id) && !!seen.add(pg.game.id));
65
+ }
66
+
67
+ function title(cfg: LeagueConfig, text: string): void {
68
+ console.log(c.bold(c.cyan(`${cfg.icon} ${cfg.label} — ${text}`)));
69
+ }
70
+
71
+ /** Print the real bracket for `season`; false if ESPN has no postseason games for it. */
72
+ async function showPostseason(
73
+ cfg: LeagueConfig,
74
+ fmt: BracketFormat,
75
+ teams: EspnTeam[],
76
+ season: number,
77
+ favIds: ReadonlySet<string>,
78
+ ): Promise<boolean> {
79
+ const [games, standings] = await Promise.all([
80
+ loadPostseason(cfg, fmt, teams, season),
81
+ getStandings(cfg.league, { season, seasonType: SEASON_TYPES.regular }),
82
+ ]);
83
+ // ESPN's final playoffSeed already reflects the NBA play-in.
84
+ const seeds = new Map<string, string>();
85
+ for (const g of standings.groups)
86
+ for (const e of g.entries) if (Number(e.stats.playoffSeed) > 0) seeds.set(e.teamId, String(Number(e.stats.playoffSeed)));
87
+ const series = buildSeries(games, seeds);
88
+ const playIn = games.filter((pg) => pg.playIn);
89
+ if (series.length === 0 && playIn.length === 0) return false;
90
+
91
+ title(cfg, `Playoff Bracket ${standings.seasonName || seasonLabel(season)}`);
92
+ console.log(renderBracket(fmt, series, playIn, favIds));
93
+ return true;
94
+ }
95
+
96
+ /** Print a bracket projected from standings: current ones once the regular
97
+ * season is under way, else last season's final standings. */
98
+ async function showProjection(cfg: LeagueConfig, fmt: BracketFormat, favIds: ReadonlySet<string>): Promise<void> {
99
+ const level = fmt.projection === "nhl" ? "division" : "conference";
100
+ const view = await loadStandingsView((q) => getStandings(cfg.league, q), level);
101
+ if (view.kind === "not-started") {
102
+ title(cfg, `Projected Playoff Bracket${view.upcoming ? ` ${view.upcoming}` : ""}`);
103
+ console.log(c.dim("\nThe regular season hasn't started, and ESPN has no earlier standings to project from."));
104
+ return;
105
+ }
106
+ const groups = view.standings.groups;
107
+ const projected = fmt.projection === "nhl" ? projectNhl(groups, fmt.rankBy) : projectNba(groups, fmt.rankBy);
108
+ title(cfg, `Projected Playoff Bracket ${view.kind === "current" ? view.standings.seasonName : view.upcoming}`.trimEnd());
109
+ if (view.kind === "current") {
110
+ console.log(c.yellow("PROJECTED — the postseason hasn't started; seeded from current standings."));
111
+ } else {
112
+ const last = view.standings.season;
113
+ console.log(
114
+ c.yellow(
115
+ `PROJECTED — the ${view.upcoming || "new"} regular season hasn't started; seeded from last season's (${view.standings.seasonName}) final standings.`,
116
+ ),
117
+ );
118
+ if (last !== null) console.log(c.dim(`Last season's actual bracket: sportsing ${cfg.sport} bracket --season ${last}`));
119
+ }
120
+ if (projected.length === 0) {
121
+ console.log(c.dim("\nNot enough standings data to project a bracket."));
122
+ return;
123
+ }
124
+ console.log(renderProjection(fmt, projected, favIds));
125
+ }
126
+
127
+ /** `bracket [--season YYYY]`. */
128
+ export async function leagueBracket(cfg: LeagueConfig, args: string[], favoriteIds: FavoriteIdsFn): Promise<void> {
129
+ const fmt = BRACKET_FORMATS[cfg.league];
130
+ if (!fmt) throw new Error(`No playoff format for ${cfg.label}.`);
131
+ const seasonArg = getFlag(args, "--season");
132
+ const asked = seasonArg === null ? null : parseSeasonArg(seasonArg);
133
+ if (seasonArg !== null && asked === null) {
134
+ throw new Error(`Bad --season "${seasonArg}". Use the year the season ends (2026) or 2025-26.`);
135
+ }
136
+
137
+ const [teams, favs] = await Promise.all([getTeams(cfg.league), getFavorites(cfg.sport)]);
138
+ const favIds = favs.length > 0 ? favoriteIds(cfg, teams, favs) : new Set<string>();
139
+
140
+ if (asked !== null) {
141
+ if (await showPostseason(cfg, fmt, teams, asked, favIds)) return;
142
+ title(cfg, `Playoff Bracket ${seasonLabel(asked)}`);
143
+ console.log(c.dim(`\nESPN has no ${cfg.label} postseason games for ${seasonLabel(asked)}. Drop --season for a projection.`));
144
+ return;
145
+ }
146
+
147
+ const current = await getLeagueSeason(cfg.league);
148
+ if (current && BRACKET_PHASES.has(current.type) && (await showPostseason(cfg, fmt, teams, current.year, favIds))) return;
149
+ await showProjection(cfg, fmt, favIds);
150
+ }