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
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* StatsBomb Open Data
|
|
2
|
+
* StatsBomb adapter — free Open Data by default, optional paid API.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (https://statsbomb.com/media-pack/). Pass that requirement downstream.
|
|
4
|
+
* Free (no login):
|
|
5
|
+
* https://github.com/statsbomb/open-data
|
|
6
|
+
* Credit StatsBomb in published analysis (https://statsbomb.com/media-pack/).
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Paid (customer login — same as statsbombpy):
|
|
9
|
+
* SB_USERNAME + SB_PASSWORD → https://data.statsbombservices.com
|
|
10
|
+
* Coverage and freshness follow your StatsBomb contract.
|
|
11
|
+
*
|
|
12
|
+
* Women's competitions only (`competition_gender === "female"`).
|
|
11
13
|
*/
|
|
14
|
+
import { DEFAULT_STATSBOMB_API_BASE, } from "../credentials.js";
|
|
12
15
|
import { aggregateStatsBombPlayerMatch } from "./statsbomb-player-stats.js";
|
|
13
16
|
import { entityId } from "../ids.js";
|
|
17
|
+
import { statsBombPaidUrls, paidExtrasSummary, } from "./statsbomb-endpoints.js";
|
|
14
18
|
const SOURCE = "statsbomb";
|
|
15
|
-
const
|
|
19
|
+
const OPEN_DATA_BASE = "https://raw.githubusercontent.com/statsbomb/open-data/master/data";
|
|
16
20
|
function normalizeName(name) {
|
|
17
21
|
return name.trim().toLowerCase();
|
|
18
22
|
}
|
|
19
|
-
|
|
23
|
+
function asArray(payload) {
|
|
24
|
+
if (payload == null)
|
|
25
|
+
return [];
|
|
26
|
+
if (Array.isArray(payload))
|
|
27
|
+
return payload;
|
|
28
|
+
return Object.values(payload);
|
|
29
|
+
}
|
|
30
|
+
function basicAuthHeader(username, password) {
|
|
31
|
+
return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
|
|
32
|
+
}
|
|
33
|
+
async function defaultOpenFetchJson(url) {
|
|
20
34
|
const res = await fetch(url);
|
|
21
35
|
if (!res.ok) {
|
|
22
36
|
throw new Error(`StatsBomb fetch failed (${res.status}): ${url}`);
|
|
@@ -25,19 +39,84 @@ async function defaultFetchJson(url) {
|
|
|
25
39
|
}
|
|
26
40
|
export class StatsBombSource {
|
|
27
41
|
id = SOURCE;
|
|
28
|
-
|
|
42
|
+
accessMode;
|
|
43
|
+
openDataBaseUrl;
|
|
44
|
+
apiBaseUrl;
|
|
45
|
+
username;
|
|
46
|
+
password;
|
|
29
47
|
fetchJson;
|
|
30
48
|
includePlayerStats;
|
|
31
49
|
playerStatsLimit;
|
|
50
|
+
paidExtras;
|
|
32
51
|
constructor(options = {}) {
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
const username = options.credentials?.username?.trim();
|
|
53
|
+
const password = options.credentials?.password?.trim();
|
|
54
|
+
this.accessMode = username && password ? "paid" : "open-data";
|
|
55
|
+
this.username = username;
|
|
56
|
+
this.password = password;
|
|
57
|
+
this.openDataBaseUrl =
|
|
58
|
+
options.openDataBaseUrl ?? options.baseUrl ?? OPEN_DATA_BASE;
|
|
59
|
+
this.apiBaseUrl = (options.apiBaseUrl ??
|
|
60
|
+
options.credentials?.apiBaseUrl ??
|
|
61
|
+
DEFAULT_STATSBOMB_API_BASE).replace(/\/$/, "");
|
|
35
62
|
this.includePlayerStats = options.includePlayerStats ?? false;
|
|
36
63
|
this.playerStatsLimit = options.playerStatsLimit ?? 5;
|
|
64
|
+
this.paidExtras = options.paidExtras ?? {};
|
|
65
|
+
if (options.fetchJson) {
|
|
66
|
+
this.fetchJson = options.fetchJson;
|
|
67
|
+
}
|
|
68
|
+
else if (this.accessMode === "paid") {
|
|
69
|
+
const user = username;
|
|
70
|
+
const pass = password;
|
|
71
|
+
this.fetchJson = async (url) => {
|
|
72
|
+
const res = await fetch(url, {
|
|
73
|
+
headers: {
|
|
74
|
+
accept: "application/json",
|
|
75
|
+
authorization: basicAuthHeader(user, pass),
|
|
76
|
+
"user-agent": "campus-stats (StatsBomb customer BYOK; +https://github.com/Minacava/campus-stats)",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
throw new Error(`StatsBomb paid API HTTP ${res.status} for ${url}. ` +
|
|
81
|
+
`Check SB_USERNAME / SB_PASSWORD and that your contract covers this endpoint.`);
|
|
82
|
+
}
|
|
83
|
+
return (await res.json());
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.fetchJson = defaultOpenFetchJson;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
paidUrls() {
|
|
91
|
+
return statsBombPaidUrls(this.apiBaseUrl);
|
|
92
|
+
}
|
|
93
|
+
competitionsUrl() {
|
|
94
|
+
if (this.accessMode === "paid") {
|
|
95
|
+
return this.paidUrls().competitions();
|
|
96
|
+
}
|
|
97
|
+
return `${this.openDataBaseUrl}/competitions.json`;
|
|
98
|
+
}
|
|
99
|
+
matchesUrl(competitionId, seasonId) {
|
|
100
|
+
if (this.accessMode === "paid") {
|
|
101
|
+
return this.paidUrls().matches(competitionId, seasonId);
|
|
102
|
+
}
|
|
103
|
+
return `${this.openDataBaseUrl}/matches/${competitionId}/${seasonId}.json`;
|
|
104
|
+
}
|
|
105
|
+
lineupsUrl(matchId) {
|
|
106
|
+
if (this.accessMode === "paid") {
|
|
107
|
+
return this.paidUrls().lineups(matchId);
|
|
108
|
+
}
|
|
109
|
+
return `${this.openDataBaseUrl}/lineups/${matchId}.json`;
|
|
110
|
+
}
|
|
111
|
+
eventsUrl(matchId) {
|
|
112
|
+
if (this.accessMode === "paid") {
|
|
113
|
+
return this.paidUrls().events(matchId);
|
|
114
|
+
}
|
|
115
|
+
return `${this.openDataBaseUrl}/events/${matchId}.json`;
|
|
37
116
|
}
|
|
38
117
|
/** Catalogue of women's competitions (clubs + national-team tournaments). */
|
|
39
118
|
async listWomenCompetitions() {
|
|
40
|
-
const all = await this.fetchJson(
|
|
119
|
+
const all = asArray(await this.fetchJson(this.competitionsUrl()));
|
|
41
120
|
const byId = new Map();
|
|
42
121
|
for (const row of all) {
|
|
43
122
|
if (row.competition_gender !== "female")
|
|
@@ -59,7 +138,7 @@ export class StatsBombSource {
|
|
|
59
138
|
return [...byId.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
60
139
|
}
|
|
61
140
|
async syncCompetition(competitionName) {
|
|
62
|
-
const all = await this.fetchJson(
|
|
141
|
+
const all = asArray(await this.fetchJson(this.competitionsUrl()));
|
|
63
142
|
const rows = all.filter((c) => c.competition_gender === "female" &&
|
|
64
143
|
normalizeName(c.competition_name) === normalizeName(competitionName));
|
|
65
144
|
if (rows.length === 0) {
|
|
@@ -68,8 +147,11 @@ export class StatsBombSource {
|
|
|
68
147
|
.filter((c) => c.competition_gender === "female")
|
|
69
148
|
.map((c) => c.competition_name)),
|
|
70
149
|
].sort();
|
|
71
|
-
|
|
72
|
-
|
|
150
|
+
const modeHint = this.accessMode === "paid"
|
|
151
|
+
? "StatsBomb paid API (your contract coverage)"
|
|
152
|
+
: "StatsBomb Open Data";
|
|
153
|
+
throw new Error(`Competition not found in ${modeHint}: "${competitionName}". ` +
|
|
154
|
+
`Available: ${available.join(", ") || "(none)"}`);
|
|
73
155
|
}
|
|
74
156
|
const competitions = new Map();
|
|
75
157
|
const seasons = new Map();
|
|
@@ -99,7 +181,7 @@ export class StatsBombSource {
|
|
|
99
181
|
},
|
|
100
182
|
],
|
|
101
183
|
});
|
|
102
|
-
const matchRows = await this.fetchJson(
|
|
184
|
+
const matchRows = asArray(await this.fetchJson(this.matchesUrl(row.competition_id, row.season_id)));
|
|
103
185
|
for (const m of matchRows) {
|
|
104
186
|
const homeId = entityId(SOURCE, "team", m.home_team.home_team_id);
|
|
105
187
|
const awayId = entityId(SOURCE, "team", m.away_team.away_team_id);
|
|
@@ -144,6 +226,82 @@ export class StatsBombSource {
|
|
|
144
226
|
}
|
|
145
227
|
return result;
|
|
146
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Hit optional paid-only endpoints for a sample competition/season/match.
|
|
231
|
+
* Confirms the licence covers them; Campus does not yet map these into the
|
|
232
|
+
* canonical schema (probe only).
|
|
233
|
+
*/
|
|
234
|
+
async probePaidExtras(context) {
|
|
235
|
+
const enabled = paidExtrasSummary(this.paidExtras);
|
|
236
|
+
if (enabled.length === 0)
|
|
237
|
+
return [];
|
|
238
|
+
if (this.accessMode !== "paid") {
|
|
239
|
+
throw new Error(`Paid extras (${enabled.join(", ")}) need SB_USERNAME / SB_PASSWORD. ` +
|
|
240
|
+
`See docs/statsbomb-endpoints.md.`);
|
|
241
|
+
}
|
|
242
|
+
const urls = this.paidUrls();
|
|
243
|
+
const jobs = [];
|
|
244
|
+
if (this.paidExtras.playerSeasonStats) {
|
|
245
|
+
jobs.push({
|
|
246
|
+
id: "playerSeasonStats",
|
|
247
|
+
url: urls.playerSeasonStats(context.competitionId, context.seasonId),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (this.paidExtras.teamSeasonStats) {
|
|
251
|
+
jobs.push({
|
|
252
|
+
id: "teamSeasonStats",
|
|
253
|
+
url: urls.teamSeasonStats(context.competitionId, context.seasonId),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (context.matchId != null) {
|
|
257
|
+
if (this.paidExtras.playerMatchStats) {
|
|
258
|
+
jobs.push({
|
|
259
|
+
id: "playerMatchStats",
|
|
260
|
+
url: urls.playerMatchStats(context.matchId),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (this.paidExtras.teamMatchStats) {
|
|
264
|
+
jobs.push({
|
|
265
|
+
id: "teamMatchStats",
|
|
266
|
+
url: urls.teamMatchStats(context.matchId),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (this.paidExtras.frames360) {
|
|
270
|
+
jobs.push({
|
|
271
|
+
id: "frames360",
|
|
272
|
+
url: urls.frames360(context.matchId),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const out = [];
|
|
277
|
+
for (const job of jobs) {
|
|
278
|
+
try {
|
|
279
|
+
const payload = await this.fetchJson(job.url);
|
|
280
|
+
const itemCount = Array.isArray(payload)
|
|
281
|
+
? payload.length
|
|
282
|
+
: payload && typeof payload === "object"
|
|
283
|
+
? Object.keys(payload).length
|
|
284
|
+
: undefined;
|
|
285
|
+
out.push({
|
|
286
|
+
id: job.id,
|
|
287
|
+
ok: true,
|
|
288
|
+
url: job.url,
|
|
289
|
+
itemCount,
|
|
290
|
+
mappedToCampusSchema: false,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
out.push({
|
|
295
|
+
id: job.id,
|
|
296
|
+
ok: false,
|
|
297
|
+
url: job.url,
|
|
298
|
+
error: err instanceof Error ? err.message : String(err),
|
|
299
|
+
mappedToCampusSchema: false,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
147
305
|
/** Load aggregated v1 player stats + lineups for the given canonical matches. */
|
|
148
306
|
async loadPlayerStatsForMatches(matches) {
|
|
149
307
|
const players = new Map();
|
|
@@ -153,8 +311,8 @@ export class StatsBombSource {
|
|
|
153
311
|
const nativeId = match.sources.find((s) => s.source === SOURCE)?.id;
|
|
154
312
|
if (!nativeId)
|
|
155
313
|
continue;
|
|
156
|
-
const lineups = await this.fetchJson(
|
|
157
|
-
const events = await this.fetchJson(
|
|
314
|
+
const lineups = asArray(await this.fetchJson(this.lineupsUrl(nativeId)));
|
|
315
|
+
const events = asArray(await this.fetchJson(this.eventsUrl(nativeId)));
|
|
158
316
|
const agg = aggregateStatsBombPlayerMatch(match.id, lineups, events);
|
|
159
317
|
for (const p of agg.players)
|
|
160
318
|
players.set(p.id, p);
|
package/dist/sources/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Competition,
|
|
1
|
+
import type { Competition, LineupEntry, Match, Player, PlayerMatchStats, Season, Team } from "../types.js";
|
|
2
2
|
/** Result of syncing one competition from a provider into canonical entities. */
|
|
3
3
|
export interface SyncResult {
|
|
4
4
|
competitions: Competition[];
|
|
@@ -8,7 +8,6 @@ export interface SyncResult {
|
|
|
8
8
|
players?: Player[];
|
|
9
9
|
playerMatchStats?: PlayerMatchStats[];
|
|
10
10
|
lineups?: LineupEntry[];
|
|
11
|
-
injuries?: InjuryRecord[];
|
|
12
11
|
}
|
|
13
12
|
/**
|
|
14
13
|
* Provider adapter contract.
|
package/dist/types.d.ts
CHANGED
|
@@ -84,21 +84,6 @@ export interface LineupEntry {
|
|
|
84
84
|
jerseyNumber?: number | null;
|
|
85
85
|
sources: SourceRef[];
|
|
86
86
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Injury / availability record.
|
|
89
|
-
* Open StatsBomb data does not include injuries — this shape is reserved for
|
|
90
|
-
* future adapters; the CLI currently returns an empty list.
|
|
91
|
-
*/
|
|
92
|
-
export interface InjuryRecord {
|
|
93
|
-
id: string;
|
|
94
|
-
playerId: string;
|
|
95
|
-
teamId?: string;
|
|
96
|
-
status: "injured" | "doubtful" | "suspended" | "unknown";
|
|
97
|
-
description?: string;
|
|
98
|
-
fromDate?: string;
|
|
99
|
-
toDate?: string;
|
|
100
|
-
sources: SourceRef[];
|
|
101
|
-
}
|
|
102
87
|
/** In-memory / on-disk cache shape (JSON file in v0). */
|
|
103
88
|
export interface CampusCache {
|
|
104
89
|
competitions: Competition[];
|
|
@@ -110,5 +95,4 @@ export interface CampusCache {
|
|
|
110
95
|
players: Player[];
|
|
111
96
|
playerMatchStats: PlayerMatchStats[];
|
|
112
97
|
lineups: LineupEntry[];
|
|
113
|
-
injuries: InjuryRecord[];
|
|
114
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "campus-stats",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Campus — installable women's football data layer for apps (clubs, national teams, matches, fantasy points)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -41,11 +41,12 @@
|
|
|
41
41
|
"cli"
|
|
42
42
|
],
|
|
43
43
|
"repository": {
|
|
44
|
-
"
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/Minacava/campus-stats.git"
|
|
45
46
|
},
|
|
46
|
-
"homepage": "https://campus-
|
|
47
|
+
"homepage": "https://github.com/Minacava/campus-stats#readme",
|
|
47
48
|
"bugs": {
|
|
48
|
-
"url": "https://campus-
|
|
49
|
+
"url": "https://github.com/Minacava/campus-stats/issues"
|
|
49
50
|
},
|
|
50
51
|
"license": "MIT",
|
|
51
52
|
"devDependencies": {
|
package/dist/injuries.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Injury / availability feed.
|
|
3
|
-
*
|
|
4
|
-
* StatsBomb Open Data (and our FBref pilot) do not publish injury lists.
|
|
5
|
-
* This module keeps a stable API so fantasy apps can call `listInjuries`
|
|
6
|
-
* today and plug a future adapter without changing call sites.
|
|
7
|
-
*/
|
|
8
|
-
import type { CampusCache, InjuryRecord } from "./types.js";
|
|
9
|
-
export declare const INJURIES_AVAILABLE = false;
|
|
10
|
-
export declare const INJURIES_STATUS_MESSAGE: string;
|
|
11
|
-
/** Always empty until an injury-capable adapter is added. */
|
|
12
|
-
export declare function listInjuries(cache: CampusCache): InjuryRecord[];
|
package/dist/injuries.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Injury / availability feed.
|
|
3
|
-
*
|
|
4
|
-
* StatsBomb Open Data (and our FBref pilot) do not publish injury lists.
|
|
5
|
-
* This module keeps a stable API so fantasy apps can call `listInjuries`
|
|
6
|
-
* today and plug a future adapter without changing call sites.
|
|
7
|
-
*/
|
|
8
|
-
export const INJURIES_AVAILABLE = false;
|
|
9
|
-
export const INJURIES_STATUS_MESSAGE = "Injury feeds are not available from current open sources (StatsBomb / FBref pilot). " +
|
|
10
|
-
"campus exposes InjuryRecord + listInjuries() for future adapters; " +
|
|
11
|
-
"today the list is always empty.";
|
|
12
|
-
/** Always empty until an injury-capable adapter is added. */
|
|
13
|
-
export function listInjuries(cache) {
|
|
14
|
-
return cache.injuries ?? [];
|
|
15
|
-
}
|