fitzroy 1.1.1 → 1.3.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/dist/cli.js +802 -259
- package/dist/index.d.ts +258 -249
- package/dist/index.js +235 -87
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,6 +20,12 @@ var UnsupportedSourceError = class extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
name = "UnsupportedSourceError";
|
|
22
22
|
};
|
|
23
|
+
function aflwUnsupportedError(source) {
|
|
24
|
+
return new UnsupportedSourceError(
|
|
25
|
+
`AFLW data is not available from ${source}. Use --source afl-api for AFLW data.`,
|
|
26
|
+
source
|
|
27
|
+
);
|
|
28
|
+
}
|
|
23
29
|
var ValidationError = class extends Error {
|
|
24
30
|
constructor(message, issues) {
|
|
25
31
|
super(message);
|
|
@@ -119,6 +125,10 @@ function toAestString(date) {
|
|
|
119
125
|
});
|
|
120
126
|
return formatter.format(date);
|
|
121
127
|
}
|
|
128
|
+
function resolveDefaultSeason(competition = "AFLM") {
|
|
129
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
130
|
+
return competition === "AFLW" ? year - 1 : year;
|
|
131
|
+
}
|
|
122
132
|
var MONTH_ABBREV_TO_INDEX = /* @__PURE__ */ new Map([
|
|
123
133
|
["jan", 0],
|
|
124
134
|
["feb", 1],
|
|
@@ -233,6 +243,26 @@ function normaliseTeamName(raw) {
|
|
|
233
243
|
const trimmed = raw.trim();
|
|
234
244
|
return ALIAS_MAP.get(trimmed.toLowerCase()) ?? trimmed;
|
|
235
245
|
}
|
|
246
|
+
var AFL_API_TEAM_IDS = /* @__PURE__ */ new Map([
|
|
247
|
+
["CD_T10", "Adelaide Crows"],
|
|
248
|
+
["CD_T20", "Brisbane Lions"],
|
|
249
|
+
["CD_T30", "Carlton"],
|
|
250
|
+
["CD_T40", "Collingwood"],
|
|
251
|
+
["CD_T50", "Essendon"],
|
|
252
|
+
["CD_T60", "Fremantle"],
|
|
253
|
+
["CD_T70", "Geelong Cats"],
|
|
254
|
+
["CD_T1000", "Gold Coast Suns"],
|
|
255
|
+
["CD_T1010", "GWS Giants"],
|
|
256
|
+
["CD_T80", "Hawthorn"],
|
|
257
|
+
["CD_T90", "Melbourne"],
|
|
258
|
+
["CD_T100", "North Melbourne"],
|
|
259
|
+
["CD_T110", "Port Adelaide"],
|
|
260
|
+
["CD_T120", "Richmond"],
|
|
261
|
+
["CD_T130", "St Kilda"],
|
|
262
|
+
["CD_T160", "Sydney Swans"],
|
|
263
|
+
["CD_T150", "West Coast Eagles"],
|
|
264
|
+
["CD_T140", "Western Bulldogs"]
|
|
265
|
+
]);
|
|
236
266
|
|
|
237
267
|
// src/transforms/footywire-player-stats.ts
|
|
238
268
|
import * as cheerio from "cheerio";
|
|
@@ -487,6 +517,14 @@ var FINALS_PATTERN = /final|elimination|qualifying|preliminary|semi|grand/i;
|
|
|
487
517
|
function inferRoundType(roundName) {
|
|
488
518
|
return FINALS_PATTERN.test(roundName) ? "Finals" : "HomeAndAway";
|
|
489
519
|
}
|
|
520
|
+
function finalsRoundNumber(headerText, lastHARound) {
|
|
521
|
+
const lower = headerText.toLowerCase();
|
|
522
|
+
if (lower.includes("qualifying") || lower.includes("elimination")) return lastHARound + 1;
|
|
523
|
+
if (lower.includes("semi")) return lastHARound + 2;
|
|
524
|
+
if (lower.includes("preliminary")) return lastHARound + 3;
|
|
525
|
+
if (lower.includes("grand")) return lastHARound + 4;
|
|
526
|
+
return lastHARound + 1;
|
|
527
|
+
}
|
|
490
528
|
function toMatchStatus(raw) {
|
|
491
529
|
switch (raw) {
|
|
492
530
|
case "CONCLUDED":
|
|
@@ -783,6 +821,7 @@ function parseMatchList(html, year) {
|
|
|
783
821
|
const $ = cheerio2.load(html);
|
|
784
822
|
const results = [];
|
|
785
823
|
let currentRound = 0;
|
|
824
|
+
let lastHARound = 0;
|
|
786
825
|
let currentRoundType = "HomeAndAway";
|
|
787
826
|
$("tr").each((_i, row) => {
|
|
788
827
|
const roundHeader = $(row).find("td[colspan='7']");
|
|
@@ -792,6 +831,11 @@ function parseMatchList(html, year) {
|
|
|
792
831
|
const roundMatch = /Round\s+(\d+)/i.exec(text);
|
|
793
832
|
if (roundMatch?.[1]) {
|
|
794
833
|
currentRound = Number.parseInt(roundMatch[1], 10);
|
|
834
|
+
if (currentRoundType === "HomeAndAway") {
|
|
835
|
+
lastHARound = currentRound;
|
|
836
|
+
}
|
|
837
|
+
} else if (currentRoundType === "Finals") {
|
|
838
|
+
currentRound = finalsRoundNumber(text, lastHARound);
|
|
795
839
|
}
|
|
796
840
|
return;
|
|
797
841
|
}
|
|
@@ -862,6 +906,7 @@ function parseFixtureList(html, year) {
|
|
|
862
906
|
const $ = cheerio2.load(html);
|
|
863
907
|
const fixtures = [];
|
|
864
908
|
let currentRound = 0;
|
|
909
|
+
let lastHARound = 0;
|
|
865
910
|
let currentRoundType = "HomeAndAway";
|
|
866
911
|
let gameNumber = 0;
|
|
867
912
|
$("tr").each((_i, row) => {
|
|
@@ -872,6 +917,11 @@ function parseFixtureList(html, year) {
|
|
|
872
917
|
const roundMatch = /Round\s+(\d+)/i.exec(text);
|
|
873
918
|
if (roundMatch?.[1]) {
|
|
874
919
|
currentRound = Number.parseInt(roundMatch[1], 10);
|
|
920
|
+
if (currentRoundType === "HomeAndAway") {
|
|
921
|
+
lastHARound = currentRound;
|
|
922
|
+
}
|
|
923
|
+
} else if (currentRoundType === "Finals") {
|
|
924
|
+
currentRound = finalsRoundNumber(text, lastHARound);
|
|
875
925
|
}
|
|
876
926
|
return;
|
|
877
927
|
}
|
|
@@ -1407,6 +1457,22 @@ async function fetchCoachesVotes(query) {
|
|
|
1407
1457
|
return ok(votes);
|
|
1408
1458
|
}
|
|
1409
1459
|
|
|
1460
|
+
// src/lib/concurrency.ts
|
|
1461
|
+
async function batchedMap(items, fn, options) {
|
|
1462
|
+
const batchSize = options?.batchSize ?? 5;
|
|
1463
|
+
const delayMs = options?.delayMs ?? 0;
|
|
1464
|
+
const results = [];
|
|
1465
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
1466
|
+
const batch = items.slice(i, i + batchSize);
|
|
1467
|
+
const batchResults = await Promise.all(batch.map(fn));
|
|
1468
|
+
results.push(...batchResults);
|
|
1469
|
+
if (delayMs > 0 && i + batchSize < items.length) {
|
|
1470
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return results;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1410
1476
|
// src/lib/validation.ts
|
|
1411
1477
|
import { z } from "zod/v4";
|
|
1412
1478
|
var AflApiTokenSchema = z.object({
|
|
@@ -1512,7 +1578,14 @@ var CfsPlayerInnerSchema = z.object({
|
|
|
1512
1578
|
captain: z.boolean().optional(),
|
|
1513
1579
|
playerJumperNumber: z.number().optional()
|
|
1514
1580
|
}).passthrough();
|
|
1515
|
-
var statNum = z.
|
|
1581
|
+
var statNum = z.union([
|
|
1582
|
+
z.number(),
|
|
1583
|
+
z.string().transform((s) => {
|
|
1584
|
+
if (s === "" || s === "-") return null;
|
|
1585
|
+
const n = Number(s);
|
|
1586
|
+
return Number.isNaN(n) ? null : n;
|
|
1587
|
+
})
|
|
1588
|
+
]).nullable().optional();
|
|
1516
1589
|
var PlayerGameStatsSchema = z.object({
|
|
1517
1590
|
goals: statNum,
|
|
1518
1591
|
behinds: statNum,
|
|
@@ -1590,8 +1663,8 @@ var PlayerStatsItemSchema = z.object({
|
|
|
1590
1663
|
teamId: z.string(),
|
|
1591
1664
|
playerStats: z.object({
|
|
1592
1665
|
stats: PlayerGameStatsSchema,
|
|
1593
|
-
timeOnGroundPercentage:
|
|
1594
|
-
}).passthrough()
|
|
1666
|
+
timeOnGroundPercentage: statNum
|
|
1667
|
+
}).passthrough().nullable().optional()
|
|
1595
1668
|
}).passthrough();
|
|
1596
1669
|
var PlayerStatsListSchema = z.object({
|
|
1597
1670
|
homeTeamPlayerStats: z.array(PlayerStatsItemSchema),
|
|
@@ -1692,6 +1765,7 @@ var AflApiClient = class {
|
|
|
1692
1765
|
fetchFn;
|
|
1693
1766
|
tokenUrl;
|
|
1694
1767
|
cachedToken = null;
|
|
1768
|
+
pendingAuth = null;
|
|
1695
1769
|
constructor(options) {
|
|
1696
1770
|
this.fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
1697
1771
|
this.tokenUrl = options?.tokenUrl ?? TOKEN_URL;
|
|
@@ -1699,9 +1773,21 @@ var AflApiClient = class {
|
|
|
1699
1773
|
/**
|
|
1700
1774
|
* Authenticate with the WMCTok token endpoint and cache the token.
|
|
1701
1775
|
*
|
|
1776
|
+
* Concurrent callers share the same in-flight request to avoid
|
|
1777
|
+
* redundant token fetches (thundering herd prevention).
|
|
1778
|
+
*
|
|
1702
1779
|
* @returns The access token on success, or an error Result.
|
|
1703
1780
|
*/
|
|
1704
1781
|
async authenticate() {
|
|
1782
|
+
if (this.pendingAuth) {
|
|
1783
|
+
return this.pendingAuth;
|
|
1784
|
+
}
|
|
1785
|
+
this.pendingAuth = this.doAuthenticate().finally(() => {
|
|
1786
|
+
this.pendingAuth = null;
|
|
1787
|
+
});
|
|
1788
|
+
return this.pendingAuth;
|
|
1789
|
+
}
|
|
1790
|
+
async doAuthenticate() {
|
|
1705
1791
|
try {
|
|
1706
1792
|
const response = await this.fetchFn(this.tokenUrl, {
|
|
1707
1793
|
method: "POST",
|
|
@@ -1956,7 +2042,7 @@ var AflApiClient = class {
|
|
|
1956
2042
|
return roundsResult;
|
|
1957
2043
|
}
|
|
1958
2044
|
const providerIds = roundsResult.data.flatMap((r) => r.providerId ? [r.providerId] : []);
|
|
1959
|
-
const results = await
|
|
2045
|
+
const results = await batchedMap(providerIds, (id) => this.fetchRoundMatchItems(id));
|
|
1960
2046
|
const allItems = [];
|
|
1961
2047
|
for (const result of results) {
|
|
1962
2048
|
if (!result.success) {
|
|
@@ -2255,12 +2341,14 @@ function toFixture(item, season, fallbackRoundNumber, competition) {
|
|
|
2255
2341
|
async function fetchFixture(query) {
|
|
2256
2342
|
const competition = query.competition ?? "AFLM";
|
|
2257
2343
|
if (query.source === "squiggle") {
|
|
2344
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("squiggle"));
|
|
2258
2345
|
const client2 = new SquiggleClient();
|
|
2259
2346
|
const result = await client2.fetchGames(query.season, query.round ?? void 0);
|
|
2260
2347
|
if (!result.success) return result;
|
|
2261
2348
|
return ok(transformSquiggleGamesToFixture(result.data.games, query.season));
|
|
2262
2349
|
}
|
|
2263
2350
|
if (query.source === "footywire") {
|
|
2351
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("footywire"));
|
|
2264
2352
|
const fwClient = new FootyWireClient();
|
|
2265
2353
|
const result = await fwClient.fetchSeasonFixture(query.season);
|
|
2266
2354
|
if (!result.success) return result;
|
|
@@ -2290,8 +2378,9 @@ async function fetchFixture(query) {
|
|
|
2290
2378
|
const roundProviderIds = roundsResult.data.flatMap(
|
|
2291
2379
|
(r) => r.providerId ? [{ providerId: r.providerId, roundNumber: r.roundNumber }] : []
|
|
2292
2380
|
);
|
|
2293
|
-
const roundResults = await
|
|
2294
|
-
roundProviderIds
|
|
2381
|
+
const roundResults = await batchedMap(
|
|
2382
|
+
roundProviderIds,
|
|
2383
|
+
(r) => client.fetchRoundMatchItems(r.providerId)
|
|
2295
2384
|
);
|
|
2296
2385
|
const fixtures = [];
|
|
2297
2386
|
for (let i = 0; i < roundResults.length; i++) {
|
|
@@ -2603,6 +2692,7 @@ function parseSeasonPage(html, year) {
|
|
|
2603
2692
|
const results = [];
|
|
2604
2693
|
let currentRound = 0;
|
|
2605
2694
|
let currentRoundType = "HomeAndAway";
|
|
2695
|
+
let lastHARound = 0;
|
|
2606
2696
|
let matchCounter = 0;
|
|
2607
2697
|
$("table").each((_i, table) => {
|
|
2608
2698
|
const $table = $(table);
|
|
@@ -2612,10 +2702,14 @@ function parseSeasonPage(html, year) {
|
|
|
2612
2702
|
if (roundMatch?.[1] && border !== "1") {
|
|
2613
2703
|
currentRound = Number.parseInt(roundMatch[1], 10);
|
|
2614
2704
|
currentRoundType = inferRoundType(text);
|
|
2705
|
+
if (currentRoundType === "HomeAndAway") {
|
|
2706
|
+
lastHARound = currentRound;
|
|
2707
|
+
}
|
|
2615
2708
|
return;
|
|
2616
2709
|
}
|
|
2617
2710
|
if (border !== "1" && inferRoundType(text) === "Finals") {
|
|
2618
2711
|
currentRoundType = "Finals";
|
|
2712
|
+
currentRound = finalsRoundNumber(text, lastHARound);
|
|
2619
2713
|
return;
|
|
2620
2714
|
}
|
|
2621
2715
|
if (border !== "1") return;
|
|
@@ -2930,6 +3024,7 @@ function transformLadderEntries(entries) {
|
|
|
2930
3024
|
async function fetchLadder(query) {
|
|
2931
3025
|
const competition = query.competition ?? "AFLM";
|
|
2932
3026
|
if (query.source === "squiggle") {
|
|
3027
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("squiggle"));
|
|
2933
3028
|
const client2 = new SquiggleClient();
|
|
2934
3029
|
const result = await client2.fetchStandings(query.season, query.round ?? void 0);
|
|
2935
3030
|
if (!result.success) return result;
|
|
@@ -2941,6 +3036,7 @@ async function fetchLadder(query) {
|
|
|
2941
3036
|
});
|
|
2942
3037
|
}
|
|
2943
3038
|
if (query.source === "afl-tables") {
|
|
3039
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("afl-tables"));
|
|
2944
3040
|
const atClient = new AflTablesClient();
|
|
2945
3041
|
const resultsResult = await atClient.fetchSeasonResults(query.season);
|
|
2946
3042
|
if (!resultsResult.success) return resultsResult;
|
|
@@ -3042,8 +3138,9 @@ async function fetchLineup(query) {
|
|
|
3042
3138
|
if (matchItems.data.length === 0) {
|
|
3043
3139
|
return err(new AflApiError(`No matches found for round ${query.round}`));
|
|
3044
3140
|
}
|
|
3045
|
-
const rosterResults = await
|
|
3046
|
-
matchItems.data
|
|
3141
|
+
const rosterResults = await batchedMap(
|
|
3142
|
+
matchItems.data,
|
|
3143
|
+
(item) => client.fetchMatchRoster(item.match.matchId)
|
|
3047
3144
|
);
|
|
3048
3145
|
const lineups = [];
|
|
3049
3146
|
for (const rosterResult of rosterResults) {
|
|
@@ -3074,6 +3171,7 @@ async function fetchMatchResults(query) {
|
|
|
3074
3171
|
return ok(transformMatchItems(itemsResult.data, query.season, competition));
|
|
3075
3172
|
}
|
|
3076
3173
|
case "footywire": {
|
|
3174
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("footywire"));
|
|
3077
3175
|
const client = new FootyWireClient();
|
|
3078
3176
|
const result = await client.fetchSeasonResults(query.season);
|
|
3079
3177
|
if (!result.success) return result;
|
|
@@ -3083,6 +3181,7 @@ async function fetchMatchResults(query) {
|
|
|
3083
3181
|
return result;
|
|
3084
3182
|
}
|
|
3085
3183
|
case "afl-tables": {
|
|
3184
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("afl-tables"));
|
|
3086
3185
|
const client = new AflTablesClient();
|
|
3087
3186
|
const result = await client.fetchSeasonResults(query.season);
|
|
3088
3187
|
if (!result.success) return result;
|
|
@@ -3092,6 +3191,7 @@ async function fetchMatchResults(query) {
|
|
|
3092
3191
|
return result;
|
|
3093
3192
|
}
|
|
3094
3193
|
case "squiggle": {
|
|
3194
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("squiggle"));
|
|
3095
3195
|
const client = new SquiggleClient();
|
|
3096
3196
|
const result = await client.fetchGames(query.season, query.round ?? void 0, 100);
|
|
3097
3197
|
if (!result.success) return result;
|
|
@@ -3117,7 +3217,7 @@ async function resolveTeamId(client, teamName, competition) {
|
|
|
3117
3217
|
async function fetchFromAflApi(query) {
|
|
3118
3218
|
const client = new AflApiClient();
|
|
3119
3219
|
const competition = query.competition ?? "AFLM";
|
|
3120
|
-
const season = query.season ?? (
|
|
3220
|
+
const season = query.season ?? resolveDefaultSeason(competition);
|
|
3121
3221
|
const [teamIdResult, seasonResult] = await Promise.all([
|
|
3122
3222
|
resolveTeamId(client, query.team, competition),
|
|
3123
3223
|
client.resolveCompSeason(competition, season)
|
|
@@ -3140,8 +3240,8 @@ async function fetchFromAflApi(query) {
|
|
|
3140
3240
|
jumperNumber: p.jumperNumber ?? null,
|
|
3141
3241
|
position: p.position ?? null,
|
|
3142
3242
|
dateOfBirth: p.player.dateOfBirth ?? null,
|
|
3143
|
-
heightCm: p.player.heightInCm
|
|
3144
|
-
weightKg: p.player.weightInKg
|
|
3243
|
+
heightCm: p.player.heightInCm || null,
|
|
3244
|
+
weightKg: p.player.weightInKg || null,
|
|
3145
3245
|
gamesPlayed: null,
|
|
3146
3246
|
goals: null,
|
|
3147
3247
|
draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
|
|
@@ -3155,8 +3255,9 @@ async function fetchFromAflApi(query) {
|
|
|
3155
3255
|
return ok(players);
|
|
3156
3256
|
}
|
|
3157
3257
|
async function fetchFromFootyWire(query) {
|
|
3158
|
-
const client = new FootyWireClient();
|
|
3159
3258
|
const competition = query.competition ?? "AFLM";
|
|
3259
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("footywire"));
|
|
3260
|
+
const client = new FootyWireClient();
|
|
3160
3261
|
const teamName = normaliseTeamName(query.team);
|
|
3161
3262
|
const result = await client.fetchPlayerList(teamName);
|
|
3162
3263
|
if (!result.success) return result;
|
|
@@ -3168,8 +3269,9 @@ async function fetchFromFootyWire(query) {
|
|
|
3168
3269
|
return ok(players);
|
|
3169
3270
|
}
|
|
3170
3271
|
async function fetchFromAflTables(query) {
|
|
3171
|
-
const client = new AflTablesClient();
|
|
3172
3272
|
const competition = query.competition ?? "AFLM";
|
|
3273
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("afl-tables"));
|
|
3274
|
+
const client = new AflTablesClient();
|
|
3173
3275
|
const teamName = normaliseTeamName(query.team);
|
|
3174
3276
|
const result = await client.fetchPlayerList(teamName);
|
|
3175
3277
|
if (!result.success) return result;
|
|
@@ -3204,80 +3306,82 @@ function toNullable(value) {
|
|
|
3204
3306
|
}
|
|
3205
3307
|
function transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap) {
|
|
3206
3308
|
const inner = item.player.player.player;
|
|
3207
|
-
const stats = item.playerStats
|
|
3208
|
-
const clearances = stats
|
|
3309
|
+
const stats = item.playerStats?.stats;
|
|
3310
|
+
const clearances = stats?.clearances;
|
|
3209
3311
|
return {
|
|
3210
3312
|
matchId,
|
|
3211
3313
|
season,
|
|
3212
3314
|
roundNumber,
|
|
3213
|
-
team: normaliseTeamName(
|
|
3315
|
+
team: normaliseTeamName(
|
|
3316
|
+
teamIdMap?.get(item.teamId) ?? AFL_API_TEAM_IDS.get(item.teamId) ?? item.teamId
|
|
3317
|
+
),
|
|
3214
3318
|
competition,
|
|
3215
3319
|
playerId: inner.playerId,
|
|
3216
3320
|
givenName: inner.playerName.givenName,
|
|
3217
3321
|
surname: inner.playerName.surname,
|
|
3218
3322
|
displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
|
|
3219
3323
|
jumperNumber: item.player.jumperNumber ?? null,
|
|
3220
|
-
kicks: toNullable(stats
|
|
3221
|
-
handballs: toNullable(stats
|
|
3222
|
-
disposals: toNullable(stats
|
|
3223
|
-
marks: toNullable(stats
|
|
3224
|
-
goals: toNullable(stats
|
|
3225
|
-
behinds: toNullable(stats
|
|
3226
|
-
tackles: toNullable(stats
|
|
3227
|
-
hitouts: toNullable(stats
|
|
3228
|
-
freesFor: toNullable(stats
|
|
3229
|
-
freesAgainst: toNullable(stats
|
|
3230
|
-
contestedPossessions: toNullable(stats
|
|
3231
|
-
uncontestedPossessions: toNullable(stats
|
|
3232
|
-
contestedMarks: toNullable(stats
|
|
3233
|
-
intercepts: toNullable(stats
|
|
3324
|
+
kicks: toNullable(stats?.kicks),
|
|
3325
|
+
handballs: toNullable(stats?.handballs),
|
|
3326
|
+
disposals: toNullable(stats?.disposals),
|
|
3327
|
+
marks: toNullable(stats?.marks),
|
|
3328
|
+
goals: toNullable(stats?.goals),
|
|
3329
|
+
behinds: toNullable(stats?.behinds),
|
|
3330
|
+
tackles: toNullable(stats?.tackles),
|
|
3331
|
+
hitouts: toNullable(stats?.hitouts),
|
|
3332
|
+
freesFor: toNullable(stats?.freesFor),
|
|
3333
|
+
freesAgainst: toNullable(stats?.freesAgainst),
|
|
3334
|
+
contestedPossessions: toNullable(stats?.contestedPossessions),
|
|
3335
|
+
uncontestedPossessions: toNullable(stats?.uncontestedPossessions),
|
|
3336
|
+
contestedMarks: toNullable(stats?.contestedMarks),
|
|
3337
|
+
intercepts: toNullable(stats?.intercepts),
|
|
3234
3338
|
centreClearances: toNullable(clearances?.centreClearances),
|
|
3235
3339
|
stoppageClearances: toNullable(clearances?.stoppageClearances),
|
|
3236
3340
|
totalClearances: toNullable(clearances?.totalClearances),
|
|
3237
|
-
inside50s: toNullable(stats
|
|
3238
|
-
rebound50s: toNullable(stats
|
|
3239
|
-
clangers: toNullable(stats
|
|
3240
|
-
turnovers: toNullable(stats
|
|
3241
|
-
onePercenters: toNullable(stats
|
|
3242
|
-
bounces: toNullable(stats
|
|
3243
|
-
goalAssists: toNullable(stats
|
|
3244
|
-
disposalEfficiency: toNullable(stats
|
|
3245
|
-
metresGained: toNullable(stats
|
|
3246
|
-
goalAccuracy: toNullable(stats
|
|
3247
|
-
marksInside50: toNullable(stats
|
|
3248
|
-
tacklesInside50: toNullable(stats
|
|
3249
|
-
shotsAtGoal: toNullable(stats
|
|
3250
|
-
scoreInvolvements: toNullable(stats
|
|
3251
|
-
totalPossessions: toNullable(stats
|
|
3252
|
-
timeOnGroundPercentage: toNullable(item.playerStats
|
|
3253
|
-
ratingPoints: toNullable(stats
|
|
3254
|
-
dreamTeamPoints: toNullable(stats
|
|
3255
|
-
effectiveDisposals: toNullable(stats
|
|
3256
|
-
effectiveKicks: toNullable(stats
|
|
3257
|
-
kickEfficiency: toNullable(stats
|
|
3258
|
-
kickToHandballRatio: toNullable(stats
|
|
3259
|
-
pressureActs: toNullable(stats
|
|
3260
|
-
defHalfPressureActs: toNullable(stats
|
|
3261
|
-
spoils: toNullable(stats
|
|
3262
|
-
hitoutsToAdvantage: toNullable(stats
|
|
3263
|
-
hitoutWinPercentage: toNullable(stats
|
|
3264
|
-
hitoutToAdvantageRate: toNullable(stats
|
|
3265
|
-
groundBallGets: toNullable(stats
|
|
3266
|
-
f50GroundBallGets: toNullable(stats
|
|
3267
|
-
interceptMarks: toNullable(stats
|
|
3268
|
-
marksOnLead: toNullable(stats
|
|
3269
|
-
contestedPossessionRate: toNullable(stats
|
|
3270
|
-
contestOffOneOnOnes: toNullable(stats
|
|
3271
|
-
contestOffWins: toNullable(stats
|
|
3272
|
-
contestOffWinsPercentage: toNullable(stats
|
|
3273
|
-
contestDefOneOnOnes: toNullable(stats
|
|
3274
|
-
contestDefLosses: toNullable(stats
|
|
3275
|
-
contestDefLossPercentage: toNullable(stats
|
|
3276
|
-
centreBounceAttendances: toNullable(stats
|
|
3277
|
-
kickins: toNullable(stats
|
|
3278
|
-
kickinsPlayon: toNullable(stats
|
|
3279
|
-
ruckContests: toNullable(stats
|
|
3280
|
-
scoreLaunches: toNullable(stats
|
|
3341
|
+
inside50s: toNullable(stats?.inside50s),
|
|
3342
|
+
rebound50s: toNullable(stats?.rebound50s),
|
|
3343
|
+
clangers: toNullable(stats?.clangers),
|
|
3344
|
+
turnovers: toNullable(stats?.turnovers),
|
|
3345
|
+
onePercenters: toNullable(stats?.onePercenters),
|
|
3346
|
+
bounces: toNullable(stats?.bounces),
|
|
3347
|
+
goalAssists: toNullable(stats?.goalAssists),
|
|
3348
|
+
disposalEfficiency: toNullable(stats?.disposalEfficiency),
|
|
3349
|
+
metresGained: toNullable(stats?.metresGained),
|
|
3350
|
+
goalAccuracy: toNullable(stats?.goalAccuracy),
|
|
3351
|
+
marksInside50: toNullable(stats?.marksInside50),
|
|
3352
|
+
tacklesInside50: toNullable(stats?.tacklesInside50),
|
|
3353
|
+
shotsAtGoal: toNullable(stats?.shotsAtGoal),
|
|
3354
|
+
scoreInvolvements: toNullable(stats?.scoreInvolvements),
|
|
3355
|
+
totalPossessions: toNullable(stats?.totalPossessions),
|
|
3356
|
+
timeOnGroundPercentage: toNullable(item.playerStats?.timeOnGroundPercentage),
|
|
3357
|
+
ratingPoints: toNullable(stats?.ratingPoints),
|
|
3358
|
+
dreamTeamPoints: toNullable(stats?.dreamTeamPoints),
|
|
3359
|
+
effectiveDisposals: toNullable(stats?.extendedStats?.effectiveDisposals),
|
|
3360
|
+
effectiveKicks: toNullable(stats?.extendedStats?.effectiveKicks),
|
|
3361
|
+
kickEfficiency: toNullable(stats?.extendedStats?.kickEfficiency),
|
|
3362
|
+
kickToHandballRatio: toNullable(stats?.extendedStats?.kickToHandballRatio),
|
|
3363
|
+
pressureActs: toNullable(stats?.extendedStats?.pressureActs),
|
|
3364
|
+
defHalfPressureActs: toNullable(stats?.extendedStats?.defHalfPressureActs),
|
|
3365
|
+
spoils: toNullable(stats?.extendedStats?.spoils),
|
|
3366
|
+
hitoutsToAdvantage: toNullable(stats?.extendedStats?.hitoutsToAdvantage),
|
|
3367
|
+
hitoutWinPercentage: toNullable(stats?.extendedStats?.hitoutWinPercentage),
|
|
3368
|
+
hitoutToAdvantageRate: toNullable(stats?.extendedStats?.hitoutToAdvantageRate),
|
|
3369
|
+
groundBallGets: toNullable(stats?.extendedStats?.groundBallGets),
|
|
3370
|
+
f50GroundBallGets: toNullable(stats?.extendedStats?.f50GroundBallGets),
|
|
3371
|
+
interceptMarks: toNullable(stats?.extendedStats?.interceptMarks),
|
|
3372
|
+
marksOnLead: toNullable(stats?.extendedStats?.marksOnLead),
|
|
3373
|
+
contestedPossessionRate: toNullable(stats?.extendedStats?.contestedPossessionRate),
|
|
3374
|
+
contestOffOneOnOnes: toNullable(stats?.extendedStats?.contestOffOneOnOnes),
|
|
3375
|
+
contestOffWins: toNullable(stats?.extendedStats?.contestOffWins),
|
|
3376
|
+
contestOffWinsPercentage: toNullable(stats?.extendedStats?.contestOffWinsPercentage),
|
|
3377
|
+
contestDefOneOnOnes: toNullable(stats?.extendedStats?.contestDefOneOnOnes),
|
|
3378
|
+
contestDefLosses: toNullable(stats?.extendedStats?.contestDefLosses),
|
|
3379
|
+
contestDefLossPercentage: toNullable(stats?.extendedStats?.contestDefLossPercentage),
|
|
3380
|
+
centreBounceAttendances: toNullable(stats?.extendedStats?.centreBounceAttendances),
|
|
3381
|
+
kickins: toNullable(stats?.extendedStats?.kickins),
|
|
3382
|
+
kickinsPlayon: toNullable(stats?.extendedStats?.kickinsPlayon),
|
|
3383
|
+
ruckContests: toNullable(stats?.extendedStats?.ruckContests),
|
|
3384
|
+
scoreLaunches: toNullable(stats?.extendedStats?.scoreLaunches),
|
|
3281
3385
|
source
|
|
3282
3386
|
};
|
|
3283
3387
|
}
|
|
@@ -3334,8 +3438,9 @@ async function fetchPlayerStats(query) {
|
|
|
3334
3438
|
teamIdMap.set(item.match.homeTeamId, item.match.homeTeam.name);
|
|
3335
3439
|
teamIdMap.set(item.match.awayTeamId, item.match.awayTeam.name);
|
|
3336
3440
|
}
|
|
3337
|
-
const statsResults = await
|
|
3338
|
-
matchItemsResult.data
|
|
3441
|
+
const statsResults = await batchedMap(
|
|
3442
|
+
matchItemsResult.data,
|
|
3443
|
+
(item) => client.fetchPlayerStats(item.match.matchId)
|
|
3339
3444
|
);
|
|
3340
3445
|
const allStats = [];
|
|
3341
3446
|
for (let i = 0; i < statsResults.length; i++) {
|
|
@@ -3359,6 +3464,7 @@ async function fetchPlayerStats(query) {
|
|
|
3359
3464
|
return ok(allStats);
|
|
3360
3465
|
}
|
|
3361
3466
|
case "footywire": {
|
|
3467
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("footywire"));
|
|
3362
3468
|
const fwClient = new FootyWireClient();
|
|
3363
3469
|
const idsResult = await fwClient.fetchSeasonMatchIds(query.season);
|
|
3364
3470
|
if (!idsResult.success) return idsResult;
|
|
@@ -3388,6 +3494,7 @@ async function fetchPlayerStats(query) {
|
|
|
3388
3494
|
return ok(allStats);
|
|
3389
3495
|
}
|
|
3390
3496
|
case "afl-tables": {
|
|
3497
|
+
if (competition === "AFLW") return err(aflwUnsupportedError("afl-tables"));
|
|
3391
3498
|
const atClient = new AflTablesClient();
|
|
3392
3499
|
const atResult = await atClient.fetchSeasonPlayerStats(query.season);
|
|
3393
3500
|
if (!atResult.success) return atResult;
|
|
@@ -3411,7 +3518,37 @@ async function fetchTeamStats(query) {
|
|
|
3411
3518
|
}
|
|
3412
3519
|
case "afl-tables": {
|
|
3413
3520
|
const client = new AflTablesClient();
|
|
3414
|
-
|
|
3521
|
+
const statsResult = await client.fetchTeamStats(query.season);
|
|
3522
|
+
if (!statsResult.success) return statsResult;
|
|
3523
|
+
const needsGp = statsResult.data.some((e) => e.gamesPlayed === 0);
|
|
3524
|
+
const gpMap = /* @__PURE__ */ new Map();
|
|
3525
|
+
if (needsGp) {
|
|
3526
|
+
const resultsResult = await client.fetchSeasonResults(query.season);
|
|
3527
|
+
if (resultsResult.success) {
|
|
3528
|
+
for (const m of resultsResult.data) {
|
|
3529
|
+
gpMap.set(m.homeTeam, (gpMap.get(m.homeTeam) ?? 0) + 1);
|
|
3530
|
+
gpMap.set(m.awayTeam, (gpMap.get(m.awayTeam) ?? 0) + 1);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
const enriched = statsResult.data.map((entry) => ({
|
|
3535
|
+
...entry,
|
|
3536
|
+
gamesPlayed: gpMap.get(entry.team) ?? entry.gamesPlayed
|
|
3537
|
+
}));
|
|
3538
|
+
if (summaryType === "averages") {
|
|
3539
|
+
return ok(
|
|
3540
|
+
enriched.map((entry) => ({
|
|
3541
|
+
...entry,
|
|
3542
|
+
stats: Object.fromEntries(
|
|
3543
|
+
Object.entries(entry.stats).map(([k, v]) => [
|
|
3544
|
+
k,
|
|
3545
|
+
entry.gamesPlayed > 0 ? +(v / entry.gamesPlayed).toFixed(1) : 0
|
|
3546
|
+
])
|
|
3547
|
+
)
|
|
3548
|
+
}))
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
return ok(enriched);
|
|
3415
3552
|
}
|
|
3416
3553
|
case "afl-api":
|
|
3417
3554
|
case "squiggle":
|
|
@@ -3430,19 +3567,30 @@ async function fetchTeamStats(query) {
|
|
|
3430
3567
|
function teamTypeForComp(comp) {
|
|
3431
3568
|
return comp === "AFLW" ? "WOMEN" : "MEN";
|
|
3432
3569
|
}
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
const teamType = query?.teamType ?? teamTypeForComp(query?.competition ?? "AFLM");
|
|
3436
|
-
const result = await client.fetchTeams(teamType);
|
|
3437
|
-
if (!result.success) return result;
|
|
3438
|
-
const competition = query?.competition ?? "AFLM";
|
|
3439
|
-
const teams = result.data.map((t) => ({
|
|
3570
|
+
function toTeams(data, competition) {
|
|
3571
|
+
return data.map((t) => ({
|
|
3440
3572
|
teamId: String(t.id),
|
|
3441
3573
|
name: normaliseTeamName(t.name),
|
|
3442
3574
|
abbreviation: t.abbreviation ?? "",
|
|
3443
3575
|
competition
|
|
3444
3576
|
})).filter((t) => AFL_SENIOR_TEAMS.has(t.name));
|
|
3445
|
-
|
|
3577
|
+
}
|
|
3578
|
+
async function fetchTeams(query) {
|
|
3579
|
+
const client = new AflApiClient();
|
|
3580
|
+
if (!query?.competition && !query?.teamType) {
|
|
3581
|
+
const [menResult, womenResult] = await Promise.all([
|
|
3582
|
+
client.fetchTeams("MEN"),
|
|
3583
|
+
client.fetchTeams("WOMEN")
|
|
3584
|
+
]);
|
|
3585
|
+
if (!menResult.success) return menResult;
|
|
3586
|
+
if (!womenResult.success) return womenResult;
|
|
3587
|
+
return ok([...toTeams(menResult.data, "AFLM"), ...toTeams(womenResult.data, "AFLW")]);
|
|
3588
|
+
}
|
|
3589
|
+
const competition = query?.competition ?? "AFLM";
|
|
3590
|
+
const teamType = query?.teamType ?? teamTypeForComp(competition);
|
|
3591
|
+
const result = await client.fetchTeams(teamType);
|
|
3592
|
+
if (!result.success) return result;
|
|
3593
|
+
return ok(toTeams(result.data, competition));
|
|
3446
3594
|
}
|
|
3447
3595
|
async function fetchSquad(query) {
|
|
3448
3596
|
const client = new AflApiClient();
|
|
@@ -3463,8 +3611,8 @@ async function fetchSquad(query) {
|
|
|
3463
3611
|
jumperNumber: p.jumperNumber ?? null,
|
|
3464
3612
|
position: p.position ?? null,
|
|
3465
3613
|
dateOfBirth: p.player.dateOfBirth ? new Date(p.player.dateOfBirth) : null,
|
|
3466
|
-
heightCm: p.player.heightInCm
|
|
3467
|
-
weightKg: p.player.weightInKg
|
|
3614
|
+
heightCm: p.player.heightInCm || null,
|
|
3615
|
+
weightKg: p.player.weightInKg || null,
|
|
3468
3616
|
draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
|
|
3469
3617
|
draftPosition: p.player.draftPosition ? Number.parseInt(p.player.draftPosition, 10) || null : null,
|
|
3470
3618
|
draftType: p.player.draftType ?? null,
|