campus-stats 0.4.3 → 0.6.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/CHANGELOG.md +33 -0
- package/LICENSE +1 -1
- package/README.md +190 -69
- package/dist/cache.js +0 -3
- package/dist/cli.js +145 -42
- package/dist/client.d.ts +1 -6
- package/dist/client.js +3 -13
- package/dist/credentials.d.ts +91 -0
- package/dist/credentials.js +145 -0
- package/dist/data-bundle.d.ts +8 -7
- package/dist/data-bundle.js +12 -11
- package/dist/db/sqlite-store.d.ts +1 -1
- package/dist/db/sqlite-store.js +0 -36
- package/dist/fantasy.js +0 -4
- package/dist/http.js +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +3 -4
- package/dist/sources/create.d.ts +16 -0
- package/dist/sources/create.js +23 -0
- package/dist/sources/fbref.d.ts +3 -0
- package/dist/sources/fbref.js +3 -0
- package/dist/sources/index.d.ts +6 -0
- package/dist/sources/index.js +2 -0
- package/dist/sources/statsbomb-endpoints.d.ts +57 -0
- package/dist/sources/statsbomb-endpoints.js +169 -0
- package/dist/sources/statsbomb.d.ts +59 -9
- package/dist/sources/statsbomb.js +177 -19
- package/dist/sources/types.d.ts +1 -2
- package/dist/types.d.ts +0 -16
- package/package.json +5 -4
- package/dist/injuries.d.ts +0 -12
- package/dist/injuries.js +0 -15
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cachePath, emptyCache, mergeSyncResult } from "./cache.js";
|
|
3
|
-
import {
|
|
3
|
+
import { describeCredentials, resolveCredentials, } from "./credentials.js";
|
|
4
|
+
import { dataBundleUrl } from "./data-bundle.js";
|
|
4
5
|
import { readStore, resolveDbPath, resolveStoreMode, runMigrate, writeCacheToStore, writeSyncToStore, } from "./db/store-api.js";
|
|
5
6
|
import { FANTASY_PLAYER_STATS_LIMIT, listWomenCompetitions, syncFantasyBundle, updateCachedCompetitions, } from "./fantasy.js";
|
|
6
7
|
import { CampusClient } from "./client.js";
|
|
7
|
-
import { INJURIES_STATUS_MESSAGE } from "./injuries.js";
|
|
8
8
|
import { proposeIdentitiesForCompetition } from "./identity/propose.js";
|
|
9
9
|
import { setIdentityStatus } from "./identity/store.js";
|
|
10
|
-
import {
|
|
10
|
+
import { createFootballSource, KNOWN_SOURCE_IDS } from "./sources/create.js";
|
|
11
|
+
import { listStatsBombEndpoints, } from "./sources/statsbomb-endpoints.js";
|
|
11
12
|
import { StatsBombSource } from "./sources/statsbomb.js";
|
|
12
13
|
let storeMode = "json";
|
|
13
14
|
let dbPath = ".campus/campus.sqlite";
|
|
@@ -20,6 +21,8 @@ Usage:
|
|
|
20
21
|
campus update [--with-players] [--player-stats-limit <n>]
|
|
21
22
|
campus pull [--url <cache.json url>]
|
|
22
23
|
campus available
|
|
24
|
+
campus credentials
|
|
25
|
+
campus endpoints
|
|
23
26
|
campus competitions
|
|
24
27
|
campus seasons --competition <name>
|
|
25
28
|
campus teams --competition <name>
|
|
@@ -29,7 +32,6 @@ Usage:
|
|
|
29
32
|
campus lineups [--match <id>] [--team <name>]
|
|
30
33
|
campus squad --competition <name> --team <name> [--season <name>]
|
|
31
34
|
campus fantasy-points [--competition <name>] [--player <name>] [--match <id>]
|
|
32
|
-
campus injuries
|
|
33
35
|
campus identities [--status resolved|pending|rejected]
|
|
34
36
|
campus identities propose --competition <name>
|
|
35
37
|
campus identities confirm --id <identityId>
|
|
@@ -39,8 +41,15 @@ Usage:
|
|
|
39
41
|
Options:
|
|
40
42
|
--fantasy / --all Sync all StatsBomb women's comps (clubs + national teams)
|
|
41
43
|
--competition <name> Competition display name (e.g. "Liga F")
|
|
42
|
-
--source <id> Data source
|
|
43
|
-
--
|
|
44
|
+
--source <id> Data source (default: statsbomb). Free or paid if login set.
|
|
45
|
+
--sb-user <email> StatsBomb paid API username (or SB_USERNAME)
|
|
46
|
+
--sb-password <pass> StatsBomb paid API password (or SB_PASSWORD)
|
|
47
|
+
--with-players StatsBomb: also sync lineups/events → v1 player stats
|
|
48
|
+
--with-paid-player-match-stats Paid API probe: official player-match aggregates
|
|
49
|
+
--with-paid-team-match-stats Paid API probe: team-match aggregates
|
|
50
|
+
--with-paid-player-season-stats Paid API probe: player season aggregates
|
|
51
|
+
--with-paid-team-season-stats Paid API probe: team season aggregates
|
|
52
|
+
--with-paid-360 Paid API probe: 360 frames (needs 360 licence)
|
|
44
53
|
--player-stats-limit <n> Max matches to enrich (default 5; fantasy default 25)
|
|
45
54
|
--url <url> Override data-bundle URL for pull
|
|
46
55
|
--season <name> Season label (e.g. "2023/2024")
|
|
@@ -55,7 +64,9 @@ Options:
|
|
|
55
64
|
--json Force JSON cache (default)
|
|
56
65
|
--help Show this help
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
Sources: ${KNOWN_SOURCE_IDS.join(", ")}. Free by default; StatsBomb paid login → paid API.
|
|
68
|
+
See: campus endpoints · campus credentials
|
|
69
|
+
Periodic refresh: see docs/cron.md (GitHub Actions schedule + campus pull).
|
|
59
70
|
`);
|
|
60
71
|
process.exit(1);
|
|
61
72
|
}
|
|
@@ -84,56 +95,144 @@ function requireCompetition(cache, name) {
|
|
|
84
95
|
}
|
|
85
96
|
return chosen;
|
|
86
97
|
}
|
|
98
|
+
async function resolveCliCredentials(args) {
|
|
99
|
+
return resolveCredentials({
|
|
100
|
+
statsbomb: {
|
|
101
|
+
username: getFlag(args, "--sb-user"),
|
|
102
|
+
password: getFlag(args, "--sb-password"),
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function resolvePaidExtras(args) {
|
|
107
|
+
return {
|
|
108
|
+
playerMatchStats: hasFlag(args, "--with-paid-player-match-stats"),
|
|
109
|
+
teamMatchStats: hasFlag(args, "--with-paid-team-match-stats"),
|
|
110
|
+
playerSeasonStats: hasFlag(args, "--with-paid-player-season-stats"),
|
|
111
|
+
teamSeasonStats: hasFlag(args, "--with-paid-team-season-stats"),
|
|
112
|
+
frames360: hasFlag(args, "--with-paid-360"),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function paidExtrasRequested(extras) {
|
|
116
|
+
return Boolean(extras.playerMatchStats ||
|
|
117
|
+
extras.teamMatchStats ||
|
|
118
|
+
extras.playerSeasonStats ||
|
|
119
|
+
extras.teamSeasonStats ||
|
|
120
|
+
extras.frames360);
|
|
121
|
+
}
|
|
87
122
|
async function cmdSync(args) {
|
|
88
123
|
const fantasy = hasFlag(args, "--fantasy") || hasFlag(args, "--all");
|
|
89
124
|
const competition = getFlag(args, "--competition");
|
|
90
125
|
const withPlayers = hasFlag(args, "--with-players");
|
|
126
|
+
const paidExtras = resolvePaidExtras(args);
|
|
91
127
|
const limitRaw = getFlag(args, "--player-stats-limit");
|
|
92
128
|
const playerStatsLimit = limitRaw
|
|
93
129
|
? Number(limitRaw)
|
|
94
130
|
: fantasy
|
|
95
131
|
? FANTASY_PLAYER_STATS_LIMIT
|
|
96
132
|
: 5;
|
|
133
|
+
const creds = await resolveCliCredentials(args);
|
|
134
|
+
const sbCredentials = creds.statsbombPaidReady
|
|
135
|
+
? {
|
|
136
|
+
username: creds.statsbomb.username,
|
|
137
|
+
password: creds.statsbomb.password,
|
|
138
|
+
apiBaseUrl: creds.statsbomb.apiBaseUrl,
|
|
139
|
+
}
|
|
140
|
+
: undefined;
|
|
141
|
+
if (paidExtrasRequested(paidExtras) && !sbCredentials) {
|
|
142
|
+
throw new Error("Paid extras flags need SB_USERNAME / SB_PASSWORD (or --sb-user / --sb-password). " +
|
|
143
|
+
"See docs/statsbomb-endpoints.md.");
|
|
144
|
+
}
|
|
97
145
|
if (fantasy) {
|
|
98
146
|
if (competition) {
|
|
99
147
|
throw new Error("Use either --fantasy/--all or --competition, not both.");
|
|
100
148
|
}
|
|
101
|
-
|
|
149
|
+
const mode = sbCredentials ? "paid API" : "Open Data";
|
|
150
|
+
console.error(`Syncing fantasy bundle (all StatsBomb women's competitions, ${mode})…`);
|
|
102
151
|
const result = await syncFantasyBundle({
|
|
103
152
|
includePlayerStats: withPlayers,
|
|
104
153
|
playerStatsLimit,
|
|
105
154
|
latestSeasonPlayersOnly: true,
|
|
155
|
+
credentials: sbCredentials,
|
|
156
|
+
paidExtras,
|
|
106
157
|
onProgress: (msg) => console.error(msg),
|
|
107
158
|
});
|
|
108
159
|
const cache = await writeSyncToStore(storeMode, dbPath, result);
|
|
109
|
-
printSyncSummary(
|
|
160
|
+
printSyncSummary(`statsbomb:${sbCredentials ? "paid" : "open-data"}`, result, cache);
|
|
110
161
|
return;
|
|
111
162
|
}
|
|
112
163
|
if (!competition)
|
|
113
164
|
usage();
|
|
114
165
|
const sourceId = (getFlag(args, "--source") ?? "statsbomb").toLowerCase();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
source = new StatsBombSource({
|
|
118
|
-
includePlayerStats: withPlayers,
|
|
119
|
-
playerStatsLimit,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
else if (sourceId === "fbref") {
|
|
123
|
-
if (withPlayers) {
|
|
124
|
-
throw new Error("FBref player stats are not available yet (see docs/fbref-player-stats-deferred.md).");
|
|
125
|
-
}
|
|
126
|
-
source = new FbrefSource();
|
|
166
|
+
if (sourceId === "fbref" && withPlayers) {
|
|
167
|
+
throw new Error("FBref player stats are not available yet (see docs/fbref-player-stats-deferred.md).");
|
|
127
168
|
}
|
|
128
|
-
|
|
129
|
-
throw new Error(
|
|
169
|
+
if (sourceId === "fbref" && paidExtrasRequested(paidExtras)) {
|
|
170
|
+
throw new Error("Paid extras apply only to --source statsbomb.");
|
|
130
171
|
}
|
|
131
|
-
|
|
172
|
+
const source = createFootballSource(sourceId, {
|
|
173
|
+
statsbomb: {
|
|
174
|
+
includePlayerStats: withPlayers,
|
|
175
|
+
playerStatsLimit,
|
|
176
|
+
credentials: sbCredentials,
|
|
177
|
+
paidExtras,
|
|
178
|
+
},
|
|
179
|
+
credentials: sbCredentials,
|
|
180
|
+
});
|
|
181
|
+
const modeLabel = sourceId === "statsbomb"
|
|
182
|
+
? sbCredentials
|
|
183
|
+
? "statsbomb:paid"
|
|
184
|
+
: "statsbomb:open-data"
|
|
185
|
+
: source.id;
|
|
186
|
+
console.error(`Syncing "${competition}" from ${modeLabel}…`);
|
|
132
187
|
const result = await source.syncCompetition(competition);
|
|
133
188
|
const cache = await writeSyncToStore(storeMode, dbPath, result);
|
|
134
|
-
|
|
189
|
+
let paidExtraProbes;
|
|
190
|
+
if (paidExtrasRequested(paidExtras) &&
|
|
191
|
+
source instanceof StatsBombSource &&
|
|
192
|
+
result.competitions[0] &&
|
|
193
|
+
result.seasons[0]) {
|
|
194
|
+
const compNative = Number(result.competitions[0].sources.find((s) => s.source === "statsbomb")?.id);
|
|
195
|
+
const seasonNative = Number(result.seasons[0].sources
|
|
196
|
+
.find((s) => s.source === "statsbomb")
|
|
197
|
+
?.id.split(":")
|
|
198
|
+
.at(-1));
|
|
199
|
+
const matchNative = result.matches[0]?.sources.find((s) => s.source === "statsbomb")?.id;
|
|
200
|
+
if (Number.isFinite(compNative) && Number.isFinite(seasonNative)) {
|
|
201
|
+
console.error("Probing optional paid endpoints (not yet mapped to schema)…");
|
|
202
|
+
paidExtraProbes = await source.probePaidExtras({
|
|
203
|
+
competitionId: compNative,
|
|
204
|
+
seasonId: seasonNative,
|
|
205
|
+
matchId: matchNative,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
printSyncSummary(modeLabel, result, cache, paidExtraProbes);
|
|
210
|
+
}
|
|
211
|
+
async function cmdCredentials(args) {
|
|
212
|
+
const creds = await resolveCliCredentials(args);
|
|
213
|
+
const described = describeCredentials(creds);
|
|
214
|
+
console.log(JSON.stringify({
|
|
215
|
+
...described,
|
|
216
|
+
hint: creds.statsbombPaidReady
|
|
217
|
+
? "StatsBomb paid login detected — sync uses data.statsbombservices.com"
|
|
218
|
+
: "No StatsBomb login — sync uses free Open Data. FBref stays free HTML only.",
|
|
219
|
+
}, null, 2));
|
|
220
|
+
}
|
|
221
|
+
async function cmdEndpoints() {
|
|
222
|
+
console.log(JSON.stringify({
|
|
223
|
+
endpoints: listStatsBombEndpoints().map((e) => ({
|
|
224
|
+
id: e.id,
|
|
225
|
+
name: e.name,
|
|
226
|
+
wiredInCampus: e.wiredInCampus,
|
|
227
|
+
requiresPaidLicense: e.requiresPaidLicense,
|
|
228
|
+
availability: e.availability,
|
|
229
|
+
paidPath: e.paidPath ?? null,
|
|
230
|
+
openDataPath: e.openDataPath ?? null,
|
|
231
|
+
notes: e.notes,
|
|
232
|
+
})),
|
|
233
|
+
}, null, 2));
|
|
135
234
|
}
|
|
136
|
-
function printSyncSummary(sourceId, result, cache) {
|
|
235
|
+
function printSyncSummary(sourceId, result, cache, paidExtraProbes) {
|
|
137
236
|
console.log(JSON.stringify({
|
|
138
237
|
source: sourceId,
|
|
139
238
|
cache: storeMode === "json" ? cachePath() : dbPath,
|
|
@@ -143,6 +242,7 @@ function printSyncSummary(sourceId, result, cache) {
|
|
|
143
242
|
matches: result.matches.length,
|
|
144
243
|
players: result.players?.length ?? 0,
|
|
145
244
|
playerMatchStats: result.playerMatchStats?.length ?? 0,
|
|
245
|
+
paidExtraProbes,
|
|
146
246
|
totals: {
|
|
147
247
|
competitions: cache.competitions.length,
|
|
148
248
|
seasons: cache.seasons.length,
|
|
@@ -159,6 +259,14 @@ async function cmdUpdate(args) {
|
|
|
159
259
|
const playerStatsLimit = limitRaw
|
|
160
260
|
? Number(limitRaw)
|
|
161
261
|
: FANTASY_PLAYER_STATS_LIMIT;
|
|
262
|
+
const creds = await resolveCliCredentials(args);
|
|
263
|
+
const sbCredentials = creds.statsbombPaidReady
|
|
264
|
+
? {
|
|
265
|
+
username: creds.statsbomb.username,
|
|
266
|
+
password: creds.statsbomb.password,
|
|
267
|
+
apiBaseUrl: creds.statsbomb.apiBaseUrl,
|
|
268
|
+
}
|
|
269
|
+
: undefined;
|
|
162
270
|
const before = await readStore(storeMode, dbPath);
|
|
163
271
|
console.error(before.competitions.length === 0
|
|
164
272
|
? "Cache empty — running full fantasy sync…"
|
|
@@ -167,18 +275,19 @@ async function cmdUpdate(args) {
|
|
|
167
275
|
includePlayerStats: withPlayers,
|
|
168
276
|
playerStatsLimit,
|
|
169
277
|
latestSeasonPlayersOnly: true,
|
|
278
|
+
credentials: sbCredentials,
|
|
170
279
|
onProgress: (msg) => console.error(msg),
|
|
171
280
|
});
|
|
172
281
|
const cache = await writeSyncToStore(storeMode, dbPath, result);
|
|
173
|
-
printSyncSummary(
|
|
282
|
+
printSyncSummary(`statsbomb:${sbCredentials ? "paid" : "open-data"}`, result, cache);
|
|
174
283
|
}
|
|
175
284
|
async function cmdPull(args) {
|
|
176
|
-
const url = getFlag(args, "--url") ?? dataBundleUrl(
|
|
285
|
+
const url = getFlag(args, "--url") ?? dataBundleUrl();
|
|
177
286
|
console.error(`Pulling data bundle from ${url}…`);
|
|
178
287
|
const res = await fetch(url);
|
|
179
288
|
if (!res.ok) {
|
|
180
289
|
throw new Error(`Failed to pull data bundle (${res.status}). ` +
|
|
181
|
-
`Ensure the
|
|
290
|
+
`Ensure the GitHub Actions cron has published the data-latest release (see docs/cron.md).`);
|
|
182
291
|
}
|
|
183
292
|
const remote = (await res.json());
|
|
184
293
|
const incoming = {
|
|
@@ -191,7 +300,6 @@ async function cmdPull(args) {
|
|
|
191
300
|
players: remote.players ?? [],
|
|
192
301
|
playerMatchStats: remote.playerMatchStats ?? [],
|
|
193
302
|
lineups: remote.lineups ?? [],
|
|
194
|
-
injuries: remote.injuries ?? [],
|
|
195
303
|
};
|
|
196
304
|
const local = await readStore(storeMode, dbPath);
|
|
197
305
|
const merged = mergeSyncResult(local, {
|
|
@@ -202,7 +310,6 @@ async function cmdPull(args) {
|
|
|
202
310
|
players: incoming.players,
|
|
203
311
|
playerMatchStats: incoming.playerMatchStats,
|
|
204
312
|
lineups: incoming.lineups,
|
|
205
|
-
injuries: incoming.injuries,
|
|
206
313
|
});
|
|
207
314
|
// Preserve local identities; prefer remote entity payloads via mergeById.
|
|
208
315
|
merged.identities = local.identities.length
|
|
@@ -463,14 +570,6 @@ async function cmdFantasyPoints(args) {
|
|
|
463
570
|
breakdown: r.breakdown,
|
|
464
571
|
})), null, 2));
|
|
465
572
|
}
|
|
466
|
-
async function cmdInjuries() {
|
|
467
|
-
const client = CampusClient.fromCache(await readStore(storeMode, dbPath));
|
|
468
|
-
console.log(JSON.stringify({
|
|
469
|
-
available: client.injuries().available,
|
|
470
|
-
message: INJURIES_STATUS_MESSAGE,
|
|
471
|
-
records: client.injuries().records,
|
|
472
|
-
}, null, 2));
|
|
473
|
-
}
|
|
474
573
|
async function main() {
|
|
475
574
|
const [, , command, ...args] = process.argv;
|
|
476
575
|
if (!command || command === "--help" || args.includes("--help"))
|
|
@@ -491,6 +590,13 @@ async function main() {
|
|
|
491
590
|
case "available":
|
|
492
591
|
await cmdAvailable();
|
|
493
592
|
break;
|
|
593
|
+
case "credentials":
|
|
594
|
+
case "auth":
|
|
595
|
+
await cmdCredentials(args);
|
|
596
|
+
break;
|
|
597
|
+
case "endpoints":
|
|
598
|
+
await cmdEndpoints();
|
|
599
|
+
break;
|
|
494
600
|
case "competitions":
|
|
495
601
|
await cmdCompetitions();
|
|
496
602
|
break;
|
|
@@ -518,9 +624,6 @@ async function main() {
|
|
|
518
624
|
case "fantasy-points":
|
|
519
625
|
await cmdFantasyPoints(args);
|
|
520
626
|
break;
|
|
521
|
-
case "injuries":
|
|
522
|
-
await cmdInjuries();
|
|
523
|
-
break;
|
|
524
627
|
case "identities":
|
|
525
628
|
await cmdIdentities(args);
|
|
526
629
|
break;
|
package/dist/client.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type FantasySyncOptions } from "./fantasy.js";
|
|
7
7
|
import { type FantasyPointRow, type FantasyScoringRules } from "./scoring.js";
|
|
8
|
-
import type { CampusCache, Competition,
|
|
8
|
+
import type { CampusCache, Competition, LineupEntry, Match, Player, PlayerMatchStats, Season, Team } from "./types.js";
|
|
9
9
|
export interface SquadMember {
|
|
10
10
|
playerId: string;
|
|
11
11
|
playerName: string;
|
|
@@ -80,9 +80,4 @@ export declare class CampusClient {
|
|
|
80
80
|
team?: string;
|
|
81
81
|
rules?: FantasyScoringRules;
|
|
82
82
|
}): FantasyPointRow[];
|
|
83
|
-
injuries(): {
|
|
84
|
-
available: boolean;
|
|
85
|
-
message: string;
|
|
86
|
-
records: InjuryRecord[];
|
|
87
|
-
};
|
|
88
83
|
}
|
package/dist/client.js
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* helpers against an in-memory cache loaded via `fromBundle` / `fromCache`.
|
|
5
5
|
*/
|
|
6
6
|
import { emptyCache, loadCache, mergeSyncResult, saveCache, cachePath, } from "./cache.js";
|
|
7
|
-
import { dataBundleUrl
|
|
7
|
+
import { dataBundleUrl } from "./data-bundle.js";
|
|
8
8
|
import { syncFantasyBundle, updateCachedCompetitions, } from "./fantasy.js";
|
|
9
|
-
import { listInjuries, INJURIES_AVAILABLE, INJURIES_STATUS_MESSAGE } from "./injuries.js";
|
|
10
9
|
import { DEFAULT_FANTASY_RULES, scoreFantasyPoints, } from "./scoring.js";
|
|
11
10
|
function includesCI(haystack, needle) {
|
|
12
11
|
return haystack.toLowerCase().includes(needle.toLowerCase());
|
|
@@ -28,11 +27,11 @@ export class CampusClient {
|
|
|
28
27
|
}
|
|
29
28
|
/** Download the cron-published bundle and optionally merge with local cache. */
|
|
30
29
|
static async fromBundle(options = {}) {
|
|
31
|
-
const url = options.url ?? dataBundleUrl(
|
|
30
|
+
const url = options.url ?? dataBundleUrl();
|
|
32
31
|
const res = await fetch(url);
|
|
33
32
|
if (!res.ok) {
|
|
34
33
|
throw new Error(`Failed to load data bundle (${res.status}) from ${url}. ` +
|
|
35
|
-
"Publish via
|
|
34
|
+
"Publish via GitHub Actions schedule (docs/cron.md) or call syncFantasy() once.");
|
|
36
35
|
}
|
|
37
36
|
const remote = (await res.json());
|
|
38
37
|
const incoming = {
|
|
@@ -45,7 +44,6 @@ export class CampusClient {
|
|
|
45
44
|
players: remote.players ?? [],
|
|
46
45
|
playerMatchStats: remote.playerMatchStats ?? [],
|
|
47
46
|
lineups: remote.lineups ?? [],
|
|
48
|
-
injuries: remote.injuries ?? [],
|
|
49
47
|
};
|
|
50
48
|
const base = options.mergeWith ?? emptyCache();
|
|
51
49
|
const merged = mergeSyncResult(base, {
|
|
@@ -56,7 +54,6 @@ export class CampusClient {
|
|
|
56
54
|
players: incoming.players,
|
|
57
55
|
playerMatchStats: incoming.playerMatchStats,
|
|
58
56
|
lineups: incoming.lineups,
|
|
59
|
-
injuries: incoming.injuries,
|
|
60
57
|
});
|
|
61
58
|
merged.identities =
|
|
62
59
|
base.identities.length > 0 ? base.identities : incoming.identities;
|
|
@@ -263,11 +260,4 @@ export class CampusClient {
|
|
|
263
260
|
rules: filter?.rules ?? DEFAULT_FANTASY_RULES,
|
|
264
261
|
});
|
|
265
262
|
}
|
|
266
|
-
injuries() {
|
|
267
|
-
return {
|
|
268
|
-
available: INJURIES_AVAILABLE,
|
|
269
|
-
message: INJURIES_STATUS_MESSAGE,
|
|
270
|
-
records: listInjuries(this.cache),
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
263
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional credentials so Campus can use free open feeds by default, or a
|
|
3
|
+
* customer's own StatsBomb login for the paid API.
|
|
4
|
+
*
|
|
5
|
+
* Resolution order (highest wins):
|
|
6
|
+
* 1. Explicit overrides (CLI flags / library options)
|
|
7
|
+
* 2. Environment variables
|
|
8
|
+
* 3. Local config file `.campus/config.json` (gitignored under `.campus/`)
|
|
9
|
+
*
|
|
10
|
+
* StatsBomb paid API uses HTTP Basic Auth (same as statsbombpy):
|
|
11
|
+
* SB_USERNAME / SB_PASSWORD
|
|
12
|
+
*
|
|
13
|
+
* FBref / Sports Reference does **not** sell a public API key — free mode is
|
|
14
|
+
* polite HTML only. See docs/byok.md.
|
|
15
|
+
*
|
|
16
|
+
* Never log raw secrets; use `maskSecret` / `describeCredentials`.
|
|
17
|
+
*/
|
|
18
|
+
/** StatsBomb customer login (paid API). Matches statsbombpy env names. */
|
|
19
|
+
export declare const SB_USERNAME_ENV = "SB_USERNAME";
|
|
20
|
+
export declare const SB_PASSWORD_ENV = "SB_PASSWORD";
|
|
21
|
+
export declare const CAMPUS_STATSBOMB_USERNAME_ENV = "CAMPUS_STATSBOMB_USERNAME";
|
|
22
|
+
export declare const CAMPUS_STATSBOMB_PASSWORD_ENV = "CAMPUS_STATSBOMB_PASSWORD";
|
|
23
|
+
/** Optional override for the paid StatsBomb API host. */
|
|
24
|
+
export declare const CAMPUS_STATSBOMB_API_BASE_URL_ENV = "CAMPUS_STATSBOMB_API_BASE_URL";
|
|
25
|
+
export declare const DEFAULT_STATSBOMB_API_BASE = "https://data.statsbombservices.com";
|
|
26
|
+
export type CredentialSource = "override" | "env" | "config" | "none";
|
|
27
|
+
export interface StatsBombCredentials {
|
|
28
|
+
username?: string;
|
|
29
|
+
password?: string;
|
|
30
|
+
/** Paid API host (default data.statsbombservices.com). */
|
|
31
|
+
apiBaseUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface CampusConfigFile {
|
|
34
|
+
statsbomb?: {
|
|
35
|
+
username?: string;
|
|
36
|
+
password?: string;
|
|
37
|
+
apiBaseUrl?: string;
|
|
38
|
+
};
|
|
39
|
+
/** @deprecated Prefer statsbomb.username / statsbomb.password. */
|
|
40
|
+
apiKey?: string;
|
|
41
|
+
apiBaseUrl?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ResolvedCredentials {
|
|
44
|
+
statsbomb: StatsBombCredentials & {
|
|
45
|
+
usernameSource: CredentialSource;
|
|
46
|
+
passwordSource: CredentialSource;
|
|
47
|
+
apiBaseUrlSource: CredentialSource;
|
|
48
|
+
};
|
|
49
|
+
/** True when both username and password are present. */
|
|
50
|
+
statsbombPaidReady: boolean;
|
|
51
|
+
fbref: {
|
|
52
|
+
/** Always false — FBref has no official API-key product. */
|
|
53
|
+
paidApiAvailable: false;
|
|
54
|
+
note: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export interface ResolveCredentialsOptions {
|
|
58
|
+
statsbomb?: StatsBombCredentials;
|
|
59
|
+
configPath?: string;
|
|
60
|
+
configFile?: CampusConfigFile | null;
|
|
61
|
+
env?: NodeJS.ProcessEnv;
|
|
62
|
+
}
|
|
63
|
+
export declare function defaultConfigPath(cwd?: string): string;
|
|
64
|
+
export declare function loadConfigFile(filePath?: string): Promise<CampusConfigFile | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Resolve optional paid credentials. Missing StatsBomb login is fine — free
|
|
67
|
+
* open-data / FBref HTML still work.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveCredentials(options?: ResolveCredentialsOptions): Promise<ResolvedCredentials>;
|
|
70
|
+
export declare function requireStatsBombPaidLogin(creds: StatsBombCredentials): {
|
|
71
|
+
username: string;
|
|
72
|
+
password: string;
|
|
73
|
+
};
|
|
74
|
+
/** Mask a secret for CLI / logs. */
|
|
75
|
+
export declare function maskSecret(secret: string, visibleTail?: number): string;
|
|
76
|
+
export declare function describeCredentials(creds: ResolvedCredentials): {
|
|
77
|
+
statsbomb: {
|
|
78
|
+
mode: "paid" | "open-data";
|
|
79
|
+
username: string | null;
|
|
80
|
+
usernameSource: CredentialSource;
|
|
81
|
+
passwordConfigured: boolean;
|
|
82
|
+
passwordSource: CredentialSource;
|
|
83
|
+
apiBaseUrl: string;
|
|
84
|
+
apiBaseUrlSource: CredentialSource;
|
|
85
|
+
};
|
|
86
|
+
fbref: ResolvedCredentials["fbref"];
|
|
87
|
+
};
|
|
88
|
+
/** @deprecated Use requireStatsBombPaidLogin / free open-data instead. */
|
|
89
|
+
export declare function requireApiKey(_creds: {
|
|
90
|
+
apiKey?: string;
|
|
91
|
+
}, _purpose?: string): string;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional credentials so Campus can use free open feeds by default, or a
|
|
3
|
+
* customer's own StatsBomb login for the paid API.
|
|
4
|
+
*
|
|
5
|
+
* Resolution order (highest wins):
|
|
6
|
+
* 1. Explicit overrides (CLI flags / library options)
|
|
7
|
+
* 2. Environment variables
|
|
8
|
+
* 3. Local config file `.campus/config.json` (gitignored under `.campus/`)
|
|
9
|
+
*
|
|
10
|
+
* StatsBomb paid API uses HTTP Basic Auth (same as statsbombpy):
|
|
11
|
+
* SB_USERNAME / SB_PASSWORD
|
|
12
|
+
*
|
|
13
|
+
* FBref / Sports Reference does **not** sell a public API key — free mode is
|
|
14
|
+
* polite HTML only. See docs/byok.md.
|
|
15
|
+
*
|
|
16
|
+
* Never log raw secrets; use `maskSecret` / `describeCredentials`.
|
|
17
|
+
*/
|
|
18
|
+
import { readFile } from "node:fs/promises";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { DEFAULT_CACHE_DIR } from "./cache.js";
|
|
21
|
+
/** StatsBomb customer login (paid API). Matches statsbombpy env names. */
|
|
22
|
+
export const SB_USERNAME_ENV = "SB_USERNAME";
|
|
23
|
+
export const SB_PASSWORD_ENV = "SB_PASSWORD";
|
|
24
|
+
export const CAMPUS_STATSBOMB_USERNAME_ENV = "CAMPUS_STATSBOMB_USERNAME";
|
|
25
|
+
export const CAMPUS_STATSBOMB_PASSWORD_ENV = "CAMPUS_STATSBOMB_PASSWORD";
|
|
26
|
+
/** Optional override for the paid StatsBomb API host. */
|
|
27
|
+
export const CAMPUS_STATSBOMB_API_BASE_URL_ENV = "CAMPUS_STATSBOMB_API_BASE_URL";
|
|
28
|
+
export const DEFAULT_STATSBOMB_API_BASE = "https://data.statsbombservices.com";
|
|
29
|
+
function firstEnv(env, names) {
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
const value = env[name]?.trim();
|
|
32
|
+
if (value)
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
function pickField(override, envValue, configValue) {
|
|
38
|
+
if (override?.trim())
|
|
39
|
+
return { value: override.trim(), source: "override" };
|
|
40
|
+
if (envValue)
|
|
41
|
+
return { value: envValue, source: "env" };
|
|
42
|
+
if (configValue?.trim()) {
|
|
43
|
+
return { value: configValue.trim(), source: "config" };
|
|
44
|
+
}
|
|
45
|
+
return { value: undefined, source: "none" };
|
|
46
|
+
}
|
|
47
|
+
export function defaultConfigPath(cwd = process.cwd()) {
|
|
48
|
+
return path.join(cwd, DEFAULT_CACHE_DIR, "config.json");
|
|
49
|
+
}
|
|
50
|
+
export async function loadConfigFile(filePath = defaultConfigPath()) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = await readFile(filePath, "utf8");
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
if (parsed == null || typeof parsed !== "object")
|
|
55
|
+
return null;
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const code = err.code;
|
|
60
|
+
if (code === "ENOENT")
|
|
61
|
+
return null;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve optional paid credentials. Missing StatsBomb login is fine — free
|
|
67
|
+
* open-data / FBref HTML still work.
|
|
68
|
+
*/
|
|
69
|
+
export async function resolveCredentials(options = {}) {
|
|
70
|
+
const env = options.env ?? process.env;
|
|
71
|
+
const config = options.configFile !== undefined
|
|
72
|
+
? options.configFile
|
|
73
|
+
: await loadConfigFile(options.configPath ?? defaultConfigPath());
|
|
74
|
+
const sb = config?.statsbomb;
|
|
75
|
+
const username = pickField(options.statsbomb?.username, firstEnv(env, [SB_USERNAME_ENV, CAMPUS_STATSBOMB_USERNAME_ENV]), sb?.username);
|
|
76
|
+
const password = pickField(options.statsbomb?.password, firstEnv(env, [SB_PASSWORD_ENV, CAMPUS_STATSBOMB_PASSWORD_ENV]), sb?.password);
|
|
77
|
+
const apiBaseUrl = pickField(options.statsbomb?.apiBaseUrl, firstEnv(env, [CAMPUS_STATSBOMB_API_BASE_URL_ENV]), sb?.apiBaseUrl ?? config?.apiBaseUrl);
|
|
78
|
+
const statsbomb = {
|
|
79
|
+
username: username.value,
|
|
80
|
+
password: password.value,
|
|
81
|
+
apiBaseUrl: apiBaseUrl.value ?? DEFAULT_STATSBOMB_API_BASE,
|
|
82
|
+
usernameSource: username.source,
|
|
83
|
+
passwordSource: password.source,
|
|
84
|
+
apiBaseUrlSource: apiBaseUrl.source === "none" && apiBaseUrl.value === undefined
|
|
85
|
+
? "none"
|
|
86
|
+
: apiBaseUrl.value
|
|
87
|
+
? apiBaseUrl.source === "none"
|
|
88
|
+
? "none"
|
|
89
|
+
: apiBaseUrl.source
|
|
90
|
+
: "none",
|
|
91
|
+
};
|
|
92
|
+
// Default host counts as configured only when user overrode it.
|
|
93
|
+
if (!apiBaseUrl.value) {
|
|
94
|
+
statsbomb.apiBaseUrl = DEFAULT_STATSBOMB_API_BASE;
|
|
95
|
+
statsbomb.apiBaseUrlSource = "none";
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
statsbomb,
|
|
99
|
+
statsbombPaidReady: Boolean(username.value && password.value),
|
|
100
|
+
fbref: {
|
|
101
|
+
paidApiAvailable: false,
|
|
102
|
+
note: "FBref/Sports Reference does not sell a public API key. Campus uses free HTML (rate-limited). For bulk licensing contact Sports Reference directly.",
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function requireStatsBombPaidLogin(creds) {
|
|
107
|
+
const username = creds.username?.trim();
|
|
108
|
+
const password = creds.password?.trim();
|
|
109
|
+
if (username && password)
|
|
110
|
+
return { username, password };
|
|
111
|
+
throw new Error("StatsBomb paid API needs SB_USERNAME and SB_PASSWORD " +
|
|
112
|
+
"(or CAMPUS_STATSBOMB_USERNAME / CAMPUS_STATSBOMB_PASSWORD, " +
|
|
113
|
+
"or .campus/config.json statsbomb.username/password). " +
|
|
114
|
+
"Without them Campus uses free Open Data.");
|
|
115
|
+
}
|
|
116
|
+
/** Mask a secret for CLI / logs. */
|
|
117
|
+
export function maskSecret(secret, visibleTail = 4) {
|
|
118
|
+
const trimmed = secret.trim();
|
|
119
|
+
if (trimmed.length <= visibleTail)
|
|
120
|
+
return "•".repeat(trimmed.length);
|
|
121
|
+
const head = trimmed.slice(0, Math.min(8, trimmed.length - visibleTail));
|
|
122
|
+
const tail = trimmed.slice(-visibleTail);
|
|
123
|
+
return `${head}${"•".repeat(Math.max(4, trimmed.length - head.length - visibleTail))}${tail}`;
|
|
124
|
+
}
|
|
125
|
+
export function describeCredentials(creds) {
|
|
126
|
+
return {
|
|
127
|
+
statsbomb: {
|
|
128
|
+
mode: creds.statsbombPaidReady ? "paid" : "open-data",
|
|
129
|
+
username: creds.statsbomb.username
|
|
130
|
+
? maskSecret(creds.statsbomb.username)
|
|
131
|
+
: null,
|
|
132
|
+
usernameSource: creds.statsbomb.usernameSource,
|
|
133
|
+
passwordConfigured: Boolean(creds.statsbomb.password),
|
|
134
|
+
passwordSource: creds.statsbomb.passwordSource,
|
|
135
|
+
apiBaseUrl: creds.statsbomb.apiBaseUrl ?? DEFAULT_STATSBOMB_API_BASE,
|
|
136
|
+
apiBaseUrlSource: creds.statsbomb.apiBaseUrlSource,
|
|
137
|
+
},
|
|
138
|
+
fbref: creds.fbref,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** @deprecated Use requireStatsBombPaidLogin / free open-data instead. */
|
|
142
|
+
export function requireApiKey(_creds, _purpose = "paid sources") {
|
|
143
|
+
throw new Error("Generic API keys were replaced by StatsBomb login (SB_USERNAME / SB_PASSWORD). " +
|
|
144
|
+
"See docs/byok.md.");
|
|
145
|
+
}
|
package/dist/data-bundle.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Published women's fantasy data bundle (refreshed by
|
|
3
|
-
*
|
|
2
|
+
* Published women's fantasy data bundle (refreshed by GitHub Actions cron).
|
|
3
|
+
* Served as assets on the rolling Release tag `data-latest`.
|
|
4
4
|
*/
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
5
|
+
export declare const DATA_BUNDLE_OWNER = "Minacava";
|
|
6
|
+
export declare const DATA_BUNDLE_REPO = "campus-stats";
|
|
7
|
+
export declare const DATA_BUNDLE_RELEASE_TAG = "data-latest";
|
|
8
8
|
export declare const DATA_BUNDLE_FILE = "cache.json";
|
|
9
|
+
export declare const DATA_BUNDLE_META_FILE = "meta.json";
|
|
9
10
|
/** Absolute URL of the periodically refreshed cache snapshot. */
|
|
10
|
-
export declare function dataBundleUrl(
|
|
11
|
-
export declare function dataBundleMetaUrl(
|
|
11
|
+
export declare function dataBundleUrl(tag?: string): string;
|
|
12
|
+
export declare function dataBundleMetaUrl(tag?: string): string;
|