sportsing 0.1.2 → 0.2.1
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 +167 -12
- package/package.json +6 -2
- package/src/alerts.ts +47 -0
- package/src/api.ts +1 -1
- package/src/click-to-watch.ts +39 -0
- package/src/commands/_lib.ts +28 -12
- package/src/commands/analyze.ts +2 -2
- package/src/commands/ask.ts +1 -1
- package/src/commands/cross.ts +354 -0
- package/src/commands/fav.ts +19 -16
- package/src/commands/league-ai.ts +201 -0
- package/src/commands/league-bracket.ts +150 -0
- package/src/commands/league.ts +839 -0
- package/src/commands/live.ts +57 -42
- package/src/commands/me.ts +1 -1
- package/src/commands/predict.ts +2 -2
- package/src/commands/scorers.ts +1 -1
- package/src/commands/setup.ts +3 -3
- package/src/commands/subscriptions.ts +66 -0
- package/src/commands/table.ts +1 -1
- package/src/commands/upgrade.ts +137 -0
- package/src/commands/watch.ts +19 -11
- package/src/config.ts +166 -28
- package/src/espn.ts +606 -33
- package/src/format.ts +87 -0
- package/src/game-events.ts +239 -0
- package/src/game.ts +51 -0
- package/src/index.ts +41 -16
- package/src/league-ai.ts +359 -0
- package/src/league-alerts.ts +139 -0
- package/src/league-detect.ts +104 -0
- package/src/league-overlay.ts +351 -0
- package/src/league-panels.ts +159 -0
- package/src/overlay.ts +96 -51
- package/src/playoff-bracket.ts +384 -0
- package/src/recap.ts +37 -8
- package/src/season.ts +182 -0
- package/src/sports/fifa.ts +9 -4
- package/src/sports/nba.ts +20 -0
- package/src/sports/nhl.ts +20 -0
- package/src/standings.ts +130 -0
- package/src/stream.ts +49 -8
- package/src/upgrade.ts +182 -0
- package/src/watch-route.ts +95 -0
- package/src/watchability.ts +183 -0
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "os";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { mkdir, chmod } from "fs/promises";
|
|
4
|
+
import { SUBSCRIPTIONS, HOME_MARKETS, DEFAULT_HOME_MARKET, isSubscription, type Subscription } from "./watchability.ts";
|
|
4
5
|
|
|
5
6
|
const CONFIG_DIR = join(homedir(), ".config", "sportsing");
|
|
6
7
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
@@ -13,13 +14,24 @@ const LEGACY_CONFIG_FILE = join(homedir(), ".config", "sportsball", "config.json
|
|
|
13
14
|
|
|
14
15
|
interface Config {
|
|
15
16
|
apiKey?: string;
|
|
17
|
+
/** Sport-scoped favorites, `<sport>:<team>` (e.g. `nba:UTAH`, `fifa:USA`).
|
|
18
|
+
* Legacy unprefixed entries (pre-NBA/NHL) read as `fifa:<team>`. */
|
|
16
19
|
favorites?: string[];
|
|
17
|
-
/** Preferred streaming provider for `fifa watch` (peacock | fubo).
|
|
20
|
+
/** Preferred streaming provider for `fifa watch` (peacock | fubo). Predates
|
|
21
|
+
* per-sport providers, so it only ever applies to fifa. */
|
|
18
22
|
streamProvider?: string;
|
|
23
|
+
/** Preferred streaming provider per sport for `<sport> watch`, e.g.
|
|
24
|
+
* `{ "nba": "fubo" }`. Overrides the sport's built-in default. */
|
|
25
|
+
streamProviders?: Record<string, string>;
|
|
19
26
|
/** Calibrated overlay delay (seconds) per provider, to sync stats to the stream. */
|
|
20
27
|
streamDelay?: Record<string, number>;
|
|
21
|
-
/** Overlay panel choices (the gear/settings) — per provider
|
|
28
|
+
/** Overlay panel choices (the gear/settings) — per provider (FIFA) or
|
|
29
|
+
* provider:sport (e.g. "fubo:nba") → { panel: on }. */
|
|
22
30
|
overlayPanels?: Record<string, Record<string, boolean>>;
|
|
31
|
+
/** What the user can watch with (see SUBSCRIPTIONS in watchability.ts). */
|
|
32
|
+
subscriptions?: string[];
|
|
33
|
+
/** HOME_MARKETS id for blackouts / local channels; defaults to Utah. */
|
|
34
|
+
homeMarket?: string;
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
/** Default overlay panel visibility — nothing on by default, so a fresh stream
|
|
@@ -71,10 +83,18 @@ export async function setApiKey(key: string): Promise<void> {
|
|
|
71
83
|
await writeConfig(cfg);
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
/** A sport's configured streaming provider (lowercased), or null if unset:
|
|
87
|
+
* `streamProviders[sport]`, falling back to the legacy `streamProvider` for
|
|
88
|
+
* fifa only. Pure over the parsed config. */
|
|
89
|
+
export function streamProviderFor(cfg: Pick<Config, "streamProvider" | "streamProviders">, sport: string): string | null {
|
|
90
|
+
const s = sport.trim().toLowerCase();
|
|
91
|
+
const v = cfg.streamProviders?.[s] ?? (s === "fifa" ? cfg.streamProvider : undefined);
|
|
92
|
+
return v?.trim().toLowerCase() || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Preferred streaming provider for `<sport> watch`, or null if unset. */
|
|
96
|
+
export async function getStreamProvider(sport: string): Promise<string | null> {
|
|
97
|
+
return streamProviderFor(await readConfig(), sport);
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
export async function setStreamProvider(provider: string): Promise<void> {
|
|
@@ -96,11 +116,16 @@ export async function setStreamDelay(provider: string, seconds: number): Promise
|
|
|
96
116
|
await writeConfig(cfg);
|
|
97
117
|
}
|
|
98
118
|
|
|
99
|
-
/** Overlay panel visibility for a provider (defaults merged with saved choices).
|
|
100
|
-
|
|
119
|
+
/** Overlay panel visibility for a provider (defaults merged with saved choices).
|
|
120
|
+
* `provider` is the overlayPanels key: the provider (FIFA, e.g. "fubo") or
|
|
121
|
+
* provider:sport for the league overlays (e.g. "fubo:nba"), whose panels differ. */
|
|
122
|
+
export async function getOverlayPanels(
|
|
123
|
+
provider: string,
|
|
124
|
+
defaults: Record<string, boolean> = OVERLAY_PANEL_DEFAULTS,
|
|
125
|
+
): Promise<Record<string, boolean>> {
|
|
101
126
|
const cfg = await readConfig();
|
|
102
127
|
const saved = cfg.overlayPanels?.[provider.trim().toLowerCase()] ?? {};
|
|
103
|
-
return { ...
|
|
128
|
+
return { ...defaults, ...saved };
|
|
104
129
|
}
|
|
105
130
|
|
|
106
131
|
export async function setOverlayPanel(provider: string, key: string, on: boolean): Promise<void> {
|
|
@@ -111,34 +136,147 @@ export async function setOverlayPanel(provider: string, key: string, on: boolean
|
|
|
111
136
|
await writeConfig(cfg);
|
|
112
137
|
}
|
|
113
138
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
// ── Favorites ────────────────────────────────────────────────────────────────
|
|
140
|
+
// Stored as `<sport>:<team>` because team abbreviations collide across sports
|
|
141
|
+
// (the Jazz and the Mammoth are both ESPN `UTAH`). Entries written before
|
|
142
|
+
// sports were scoped are bare team names and are read as `fifa:<team>`; the
|
|
143
|
+
// next write rewrites them in prefixed form.
|
|
144
|
+
|
|
145
|
+
/** The sport a legacy (unprefixed) favorite belongs to. */
|
|
146
|
+
export const LEGACY_FAVORITE_SPORT = "fifa";
|
|
147
|
+
|
|
148
|
+
/** A stored favorite entry, split into its sport and team. */
|
|
149
|
+
export interface Favorite {
|
|
150
|
+
sport: string;
|
|
151
|
+
team: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Sport prefixes are short lowercase ids (`fifa`, `nba`, `nhl`). */
|
|
155
|
+
const PREFIXED = /^([a-z][a-z0-9]*):(.+)$/;
|
|
156
|
+
|
|
157
|
+
/** Parse a stored entry; unprefixed (legacy) entries are FIFA teams. */
|
|
158
|
+
export function parseFavorite(entry: string): Favorite {
|
|
159
|
+
const m = PREFIXED.exec(entry.trim());
|
|
160
|
+
if (m) return { sport: m[1]!, team: m[2]!.trim() };
|
|
161
|
+
return { sport: LEGACY_FAVORITE_SPORT, team: entry.trim() };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Serialize a favorite to its stored `<sport>:<team>` form. */
|
|
165
|
+
export function formatFavorite(f: Favorite): string {
|
|
166
|
+
return `${f.sport}:${f.team}`;
|
|
118
167
|
}
|
|
119
168
|
|
|
120
|
-
|
|
121
|
-
|
|
169
|
+
const normSport = (sport: string) => sport.trim().toLowerCase();
|
|
170
|
+
const sameTeam = (a: string, b: string) => a.toLowerCase() === b.trim().toLowerCase();
|
|
171
|
+
|
|
172
|
+
// Pure helpers over the stored entry list (unit-tested without touching disk).
|
|
173
|
+
|
|
174
|
+
/** A sport's team names from stored entries, in insertion order. */
|
|
175
|
+
export function favoritesFor(entries: string[], sport: string): string[] {
|
|
176
|
+
const s = normSport(sport);
|
|
177
|
+
return entries
|
|
178
|
+
.filter((e) => e.trim())
|
|
179
|
+
.map(parseFavorite)
|
|
180
|
+
.filter((f) => f.sport === s)
|
|
181
|
+
.map((f) => f.team);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Stored entries with `team` added for `sport` (unless an equal name exists in
|
|
185
|
+
* that sport, case-insensitive). Output is fully prefixed — legacy migrates. */
|
|
186
|
+
export function withFavorite(entries: string[], sport: string, team: string): { added: boolean; entries: string[] } {
|
|
187
|
+
const s = normSport(sport);
|
|
122
188
|
const name = team.trim();
|
|
189
|
+
const all = entries.filter((e) => e.trim()).map(parseFavorite);
|
|
190
|
+
const exists = all.some((f) => f.sport === s && sameTeam(f.team, name));
|
|
191
|
+
if (!exists) all.push({ sport: s, team: name });
|
|
192
|
+
return { added: !exists, entries: all.map(formatFavorite) };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Stored entries with `sport`'s `team` removed (case-insensitive); other
|
|
196
|
+
* sports are untouched. Output is fully prefixed — legacy migrates. */
|
|
197
|
+
export function withoutFavorite(entries: string[], sport: string, team: string): { removed: boolean; entries: string[] } {
|
|
198
|
+
const s = normSport(sport);
|
|
199
|
+
const all = entries.filter((e) => e.trim()).map(parseFavorite);
|
|
200
|
+
const i = all.findIndex((f) => f.sport === s && sameTeam(f.team, team));
|
|
201
|
+
if (i >= 0) all.splice(i, 1);
|
|
202
|
+
return { removed: i >= 0, entries: all.map(formatFavorite) };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A sport's favorite team names, in the order they were added (as typed). */
|
|
206
|
+
export async function getFavorites(sport: string): Promise<string[]> {
|
|
207
|
+
const cfg = await readConfig();
|
|
208
|
+
return favoritesFor(cfg.favorites ?? [], sport);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Add a favorite team for a sport. No-op (added=false) if that sport already
|
|
212
|
+
* has an equal name. Returns that sport's favorites. */
|
|
213
|
+
export async function addFavorite(sport: string, team: string): Promise<{ added: boolean; favorites: string[] }> {
|
|
123
214
|
const cfg = await readConfig();
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
if (!exists) favorites.push(name);
|
|
127
|
-
cfg.favorites = favorites;
|
|
215
|
+
const { added, entries } = withFavorite(cfg.favorites ?? [], sport, team);
|
|
216
|
+
cfg.favorites = entries;
|
|
128
217
|
await writeConfig(cfg);
|
|
129
|
-
return { added:
|
|
218
|
+
return { added, favorites: favoritesFor(entries, sport) };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Remove a sport's favorite team (case-insensitive). removed=false if it
|
|
222
|
+
* wasn't there. Returns that sport's favorites. */
|
|
223
|
+
export async function removeFavorite(sport: string, team: string): Promise<{ removed: boolean; favorites: string[] }> {
|
|
224
|
+
const cfg = await readConfig();
|
|
225
|
+
const { removed, entries } = withoutFavorite(cfg.favorites ?? [], sport, team);
|
|
226
|
+
cfg.favorites = entries;
|
|
227
|
+
await writeConfig(cfg);
|
|
228
|
+
return { removed, favorites: favoritesFor(entries, sport) };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Subscriptions + home market ──────────────────────────────────────────────
|
|
232
|
+
// Pure helpers over raw input / stored values (unit-tested without touching
|
|
233
|
+
// disk); the async get/set pair below is the IO edge.
|
|
234
|
+
|
|
235
|
+
/** Parse user input (space- and/or comma-separated ids, any case) into
|
|
236
|
+
* subscriptions in canonical order, deduped, plus anything unrecognized. */
|
|
237
|
+
export function parseSubscriptions(input: string[]): { subscriptions: Subscription[]; invalid: string[] } {
|
|
238
|
+
const tokens = input.flatMap((a) => a.split(",")).map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
239
|
+
const invalid = [...new Set(tokens.filter((t) => !isSubscription(t)))];
|
|
240
|
+
const subscriptions = SUBSCRIPTIONS.filter((s) => tokens.includes(s));
|
|
241
|
+
return { subscriptions, invalid };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Stored subscriptions, dropping anything no longer recognized. */
|
|
245
|
+
export function subscriptionsOf(stored: unknown): Subscription[] {
|
|
246
|
+
if (!Array.isArray(stored)) return [];
|
|
247
|
+
return parseSubscriptions(stored.filter((s): s is string => typeof s === "string")).subscriptions;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Normalize a home-market id; null if it isn't a known market. */
|
|
251
|
+
export function parseHomeMarket(input: string): string | null {
|
|
252
|
+
const id = input.trim().toLowerCase();
|
|
253
|
+
return Object.hasOwn(HOME_MARKETS, id) ? id : null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Stored home market, falling back to the default for unset/unknown values. */
|
|
257
|
+
export function homeMarketOf(stored: unknown): string {
|
|
258
|
+
return (typeof stored === "string" && parseHomeMarket(stored)) || DEFAULT_HOME_MARKET;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function getSubscriptions(): Promise<Subscription[]> {
|
|
262
|
+
return subscriptionsOf((await readConfig()).subscriptions);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function setSubscriptions(subscriptions: Subscription[]): Promise<void> {
|
|
266
|
+
const cfg = await readConfig();
|
|
267
|
+
cfg.subscriptions = [...subscriptions];
|
|
268
|
+
await writeConfig(cfg);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function getHomeMarket(): Promise<string> {
|
|
272
|
+
return homeMarketOf((await readConfig()).homeMarket);
|
|
130
273
|
}
|
|
131
274
|
|
|
132
|
-
/**
|
|
133
|
-
export async function
|
|
275
|
+
/** `market` must be a known HOME_MARKETS id (see parseHomeMarket). */
|
|
276
|
+
export async function setHomeMarket(market: string): Promise<void> {
|
|
134
277
|
const cfg = await readConfig();
|
|
135
|
-
|
|
136
|
-
const i = favorites.findIndex((f) => f.toLowerCase() === team.trim().toLowerCase());
|
|
137
|
-
const removed = i >= 0;
|
|
138
|
-
if (removed) favorites.splice(i, 1);
|
|
139
|
-
cfg.favorites = favorites;
|
|
278
|
+
cfg.homeMarket = market;
|
|
140
279
|
await writeConfig(cfg);
|
|
141
|
-
return { removed, favorites };
|
|
142
280
|
}
|
|
143
281
|
|
|
144
282
|
export { CONFIG_FILE };
|