fitzroy 1.0.1 → 1.1.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 +4236 -501
- package/dist/index.d.ts +1372 -914
- package/dist/index.js +2874 -1056
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,83 +1,3587 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
8
7
|
var __export = (target, all) => {
|
|
9
8
|
for (var name in all)
|
|
10
|
-
__defProp(target, name, {
|
|
11
|
-
get: all[name],
|
|
12
|
-
enumerable: true,
|
|
13
|
-
configurable: true,
|
|
14
|
-
set: __exportSetter.bind(all, name)
|
|
15
|
-
});
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
10
|
};
|
|
17
|
-
|
|
11
|
+
|
|
12
|
+
// src/lib/errors.ts
|
|
13
|
+
var AflApiError, ScrapeError, UnsupportedSourceError, ValidationError;
|
|
14
|
+
var init_errors = __esm({
|
|
15
|
+
"src/lib/errors.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
AflApiError = class extends Error {
|
|
18
|
+
constructor(message, statusCode) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.statusCode = statusCode;
|
|
21
|
+
}
|
|
22
|
+
name = "AflApiError";
|
|
23
|
+
};
|
|
24
|
+
ScrapeError = class extends Error {
|
|
25
|
+
constructor(message, source) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.source = source;
|
|
28
|
+
}
|
|
29
|
+
name = "ScrapeError";
|
|
30
|
+
};
|
|
31
|
+
UnsupportedSourceError = class extends Error {
|
|
32
|
+
constructor(message, source) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.source = source;
|
|
35
|
+
}
|
|
36
|
+
name = "UnsupportedSourceError";
|
|
37
|
+
};
|
|
38
|
+
ValidationError = class extends Error {
|
|
39
|
+
constructor(message, issues) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.issues = issues;
|
|
42
|
+
}
|
|
43
|
+
name = "ValidationError";
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// src/lib/result.ts
|
|
49
|
+
function ok(data) {
|
|
50
|
+
return { success: true, data };
|
|
51
|
+
}
|
|
52
|
+
function err(error) {
|
|
53
|
+
return { success: false, error };
|
|
54
|
+
}
|
|
55
|
+
var init_result = __esm({
|
|
56
|
+
"src/lib/result.ts"() {
|
|
57
|
+
"use strict";
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// src/lib/date-utils.ts
|
|
62
|
+
function parseFootyWireDate(dateStr) {
|
|
63
|
+
const trimmed = dateStr.trim();
|
|
64
|
+
if (trimmed === "") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
|
|
68
|
+
const normalised = withoutDow.replace(/-/g, " ");
|
|
69
|
+
const match = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
|
|
70
|
+
if (!match) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const [, dayStr, monthStr, yearStr] = match;
|
|
74
|
+
if (!dayStr || !monthStr || !yearStr) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
|
|
78
|
+
if (monthIndex === void 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const year = Number.parseInt(yearStr, 10);
|
|
82
|
+
const day = Number.parseInt(dayStr, 10);
|
|
83
|
+
const date = new Date(Date.UTC(year, monthIndex, day));
|
|
84
|
+
if (Number.isNaN(date.getTime())) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return date;
|
|
91
|
+
}
|
|
92
|
+
function parseAflTablesDate(dateStr) {
|
|
93
|
+
const trimmed = dateStr.trim();
|
|
94
|
+
if (trimmed === "") {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
|
|
98
|
+
const normalised = withoutDow.replace(/[-/]/g, " ");
|
|
99
|
+
const dmy = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
|
|
100
|
+
if (dmy) {
|
|
101
|
+
const [, dayStr, monthStr, yearStr] = dmy;
|
|
102
|
+
if (dayStr && monthStr && yearStr) {
|
|
103
|
+
return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const mdy = /^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})$/.exec(normalised);
|
|
107
|
+
if (mdy) {
|
|
108
|
+
const [, monthStr, dayStr, yearStr] = mdy;
|
|
109
|
+
if (dayStr && monthStr && yearStr) {
|
|
110
|
+
return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function buildUtcDate(year, monthStr, day) {
|
|
116
|
+
const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
|
|
117
|
+
if (monthIndex === void 0) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const date = new Date(Date.UTC(year, monthIndex, day));
|
|
121
|
+
if (Number.isNaN(date.getTime())) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return date;
|
|
128
|
+
}
|
|
129
|
+
var MONTH_ABBREV_TO_INDEX;
|
|
130
|
+
var init_date_utils = __esm({
|
|
131
|
+
"src/lib/date-utils.ts"() {
|
|
132
|
+
"use strict";
|
|
133
|
+
MONTH_ABBREV_TO_INDEX = /* @__PURE__ */ new Map([
|
|
134
|
+
["jan", 0],
|
|
135
|
+
["feb", 1],
|
|
136
|
+
["mar", 2],
|
|
137
|
+
["apr", 3],
|
|
138
|
+
["may", 4],
|
|
139
|
+
["jun", 5],
|
|
140
|
+
["jul", 6],
|
|
141
|
+
["aug", 7],
|
|
142
|
+
["sep", 8],
|
|
143
|
+
["oct", 9],
|
|
144
|
+
["nov", 10],
|
|
145
|
+
["dec", 11],
|
|
146
|
+
["january", 0],
|
|
147
|
+
["february", 1],
|
|
148
|
+
["march", 2],
|
|
149
|
+
["april", 3],
|
|
150
|
+
["june", 5],
|
|
151
|
+
["july", 6],
|
|
152
|
+
["august", 7],
|
|
153
|
+
["september", 8],
|
|
154
|
+
["october", 9],
|
|
155
|
+
["november", 10],
|
|
156
|
+
["december", 11]
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// src/lib/team-mapping.ts
|
|
162
|
+
function normaliseTeamName(raw) {
|
|
163
|
+
const trimmed = raw.trim();
|
|
164
|
+
return ALIAS_MAP.get(trimmed.toLowerCase()) ?? trimmed;
|
|
165
|
+
}
|
|
166
|
+
var TEAM_ALIASES, AFL_SENIOR_TEAMS, ALIAS_MAP;
|
|
167
|
+
var init_team_mapping = __esm({
|
|
168
|
+
"src/lib/team-mapping.ts"() {
|
|
169
|
+
"use strict";
|
|
170
|
+
TEAM_ALIASES = [
|
|
171
|
+
["Adelaide Crows", "Adelaide", "Crows", "ADEL", "AD"],
|
|
172
|
+
["Brisbane Lions", "Brisbane", "Brisbane Bears", "Bears", "Lions", "Fitzroy Lions", "BL", "BRIS"],
|
|
173
|
+
["Carlton", "Carlton Blues", "Blues", "CARL", "CA"],
|
|
174
|
+
["Collingwood", "Collingwood Magpies", "Magpies", "COLL", "CW"],
|
|
175
|
+
["Essendon", "Essendon Bombers", "Bombers", "ESS", "ES"],
|
|
176
|
+
["Fremantle", "Fremantle Dockers", "Dockers", "FRE", "FR"],
|
|
177
|
+
["Geelong Cats", "Geelong", "Cats", "GEEL", "GE"],
|
|
178
|
+
[
|
|
179
|
+
"Gold Coast Suns",
|
|
180
|
+
"Gold Coast",
|
|
181
|
+
"Gold Coast SUNS",
|
|
182
|
+
"Gold Coast Football Club",
|
|
183
|
+
"Suns",
|
|
184
|
+
"GCFC",
|
|
185
|
+
"GC"
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
"GWS Giants",
|
|
189
|
+
"GWS",
|
|
190
|
+
"GWS GIANTS",
|
|
191
|
+
"Greater Western Sydney",
|
|
192
|
+
"Giants",
|
|
193
|
+
"Greater Western Sydney Giants",
|
|
194
|
+
"GW"
|
|
195
|
+
],
|
|
196
|
+
["Hawthorn", "Hawthorn Hawks", "Hawks", "HAW", "HW"],
|
|
197
|
+
["Melbourne", "Melbourne Demons", "Demons", "MELB", "ME"],
|
|
198
|
+
["North Melbourne", "North Melbourne Kangaroos", "Kangaroos", "Kangas", "North", "NMFC", "NM"],
|
|
199
|
+
["Port Adelaide", "Port Adelaide Power", "Power", "Port", "PA", "PAFC"],
|
|
200
|
+
["Richmond", "Richmond Tigers", "Tigers", "RICH", "RI"],
|
|
201
|
+
["St Kilda", "St Kilda Saints", "Saints", "Saint Kilda", "STK", "SK"],
|
|
202
|
+
["Sydney Swans", "Sydney", "Swans", "South Melbourne", "South Melbourne Swans", "SYD", "SY"],
|
|
203
|
+
["West Coast Eagles", "West Coast", "Eagles", "WCE", "WC"],
|
|
204
|
+
["Western Bulldogs", "Bulldogs", "Footscray", "Footscray Bulldogs", "WB", "WBD"],
|
|
205
|
+
// Historical / defunct VFL teams
|
|
206
|
+
["Fitzroy", "Fitzroy Reds", "Fitzroy Gorillas", "Fitzroy Maroons", "FI"],
|
|
207
|
+
["University", "University Blacks"]
|
|
208
|
+
];
|
|
209
|
+
AFL_SENIOR_TEAMS = /* @__PURE__ */ new Set([
|
|
210
|
+
"Adelaide Crows",
|
|
211
|
+
"Brisbane Lions",
|
|
212
|
+
"Carlton",
|
|
213
|
+
"Collingwood",
|
|
214
|
+
"Essendon",
|
|
215
|
+
"Fremantle",
|
|
216
|
+
"Geelong Cats",
|
|
217
|
+
"Gold Coast Suns",
|
|
218
|
+
"GWS Giants",
|
|
219
|
+
"Hawthorn",
|
|
220
|
+
"Melbourne",
|
|
221
|
+
"North Melbourne",
|
|
222
|
+
"Port Adelaide",
|
|
223
|
+
"Richmond",
|
|
224
|
+
"St Kilda",
|
|
225
|
+
"Sydney Swans",
|
|
226
|
+
"West Coast Eagles",
|
|
227
|
+
"Western Bulldogs"
|
|
228
|
+
]);
|
|
229
|
+
ALIAS_MAP = (() => {
|
|
230
|
+
const map = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const [canonical, ...aliases] of TEAM_ALIASES) {
|
|
232
|
+
map.set(canonical.toLowerCase(), canonical);
|
|
233
|
+
for (const alias of aliases) {
|
|
234
|
+
map.set(alias.toLowerCase(), canonical);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return map;
|
|
238
|
+
})();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// src/lib/parse-utils.ts
|
|
243
|
+
function safeInt(text) {
|
|
244
|
+
const cleaned = text.replace(/[^0-9-]/g, "").trim();
|
|
245
|
+
if (!cleaned) return null;
|
|
246
|
+
const n = Number.parseInt(cleaned, 10);
|
|
247
|
+
return Number.isNaN(n) ? null : n;
|
|
248
|
+
}
|
|
249
|
+
function parseIntOr0(text) {
|
|
250
|
+
const n = Number.parseInt(text.replace(/[^0-9-]/g, ""), 10);
|
|
251
|
+
return Number.isNaN(n) ? 0 : n;
|
|
252
|
+
}
|
|
253
|
+
function parseFloatOr0(text) {
|
|
254
|
+
const n = Number.parseFloat(text.replace(/[^0-9.-]/g, ""));
|
|
255
|
+
return Number.isNaN(n) ? 0 : n;
|
|
256
|
+
}
|
|
257
|
+
var init_parse_utils = __esm({
|
|
258
|
+
"src/lib/parse-utils.ts"() {
|
|
259
|
+
"use strict";
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// src/transforms/footywire-player-stats.ts
|
|
264
|
+
import * as cheerio from "cheerio";
|
|
265
|
+
function cleanPlayerName(raw) {
|
|
266
|
+
return raw.replace(/[↗↙]/g, "").trim();
|
|
267
|
+
}
|
|
268
|
+
function parseStatsTable(html, expectedCols, rowParser) {
|
|
269
|
+
const $ = cheerio.load(html);
|
|
270
|
+
const results = [];
|
|
271
|
+
$("table").each((_i, table) => {
|
|
272
|
+
const rows = $(table).find("tr");
|
|
273
|
+
if (rows.length < 3) return;
|
|
274
|
+
const headerCells = $(rows[0]).find("td, th").map((_, c) => $(c).text().trim()).get();
|
|
275
|
+
if (headerCells[0] !== "Player" || headerCells.length < expectedCols.length) return;
|
|
276
|
+
if (!headerCells.includes(expectedCols[1])) return;
|
|
277
|
+
let teamName = "";
|
|
278
|
+
const parentTable = $(table).closest("table").parent().closest("table");
|
|
279
|
+
const teamHeader = parentTable.find("td:contains('Match Statistics')").first();
|
|
280
|
+
if (teamHeader.length > 0) {
|
|
281
|
+
const headerText = teamHeader.text().trim();
|
|
282
|
+
const match = /^(\w[\w\s]+?)\s+Match Statistics/i.exec(headerText);
|
|
283
|
+
if (match?.[1]) {
|
|
284
|
+
teamName = match[1].trim();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const parsed = [];
|
|
288
|
+
rows.each((j, row) => {
|
|
289
|
+
if (j === 0) return;
|
|
290
|
+
const cells = $(row).find("td").map((_, c) => $(c).text().trim()).get();
|
|
291
|
+
if (cells.length < expectedCols.length - 1) return;
|
|
292
|
+
const result = rowParser(cells);
|
|
293
|
+
if (result) parsed.push(result);
|
|
294
|
+
});
|
|
295
|
+
if (parsed.length > 0) {
|
|
296
|
+
results.push([teamName, parsed]);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return results;
|
|
300
|
+
}
|
|
301
|
+
function parseBasicRow(cells) {
|
|
302
|
+
const player = cleanPlayerName(cells[0] ?? "");
|
|
303
|
+
if (!player) return null;
|
|
304
|
+
return {
|
|
305
|
+
player,
|
|
306
|
+
kicks: parseIntOr0(cells[1] ?? "0"),
|
|
307
|
+
handballs: parseIntOr0(cells[2] ?? "0"),
|
|
308
|
+
disposals: parseIntOr0(cells[3] ?? "0"),
|
|
309
|
+
marks: parseIntOr0(cells[4] ?? "0"),
|
|
310
|
+
goals: parseIntOr0(cells[5] ?? "0"),
|
|
311
|
+
behinds: parseIntOr0(cells[6] ?? "0"),
|
|
312
|
+
tackles: parseIntOr0(cells[7] ?? "0"),
|
|
313
|
+
hitouts: parseIntOr0(cells[8] ?? "0"),
|
|
314
|
+
goalAssists: parseIntOr0(cells[9] ?? "0"),
|
|
315
|
+
inside50s: parseIntOr0(cells[10] ?? "0"),
|
|
316
|
+
clearances: parseIntOr0(cells[11] ?? "0"),
|
|
317
|
+
clangers: parseIntOr0(cells[12] ?? "0"),
|
|
318
|
+
rebound50s: parseIntOr0(cells[13] ?? "0"),
|
|
319
|
+
freesFor: parseIntOr0(cells[14] ?? "0"),
|
|
320
|
+
freesAgainst: parseIntOr0(cells[15] ?? "0"),
|
|
321
|
+
dreamTeamPoints: parseIntOr0(cells[16] ?? "0"),
|
|
322
|
+
supercoachPoints: parseIntOr0(cells[17] ?? "0")
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function parseAdvancedRow(cells) {
|
|
326
|
+
const player = cleanPlayerName(cells[0] ?? "");
|
|
327
|
+
if (!player) return null;
|
|
328
|
+
return {
|
|
329
|
+
player,
|
|
330
|
+
contestedPossessions: parseIntOr0(cells[1] ?? "0"),
|
|
331
|
+
uncontestedPossessions: parseIntOr0(cells[2] ?? "0"),
|
|
332
|
+
effectiveDisposals: parseIntOr0(cells[3] ?? "0"),
|
|
333
|
+
disposalEfficiency: parseFloatOr0(cells[4] ?? "0"),
|
|
334
|
+
contestedMarks: parseIntOr0(cells[5] ?? "0"),
|
|
335
|
+
goalAssists: parseIntOr0(cells[6] ?? "0"),
|
|
336
|
+
marksInside50: parseIntOr0(cells[7] ?? "0"),
|
|
337
|
+
onePercenters: parseIntOr0(cells[8] ?? "0"),
|
|
338
|
+
bounces: parseIntOr0(cells[9] ?? "0"),
|
|
339
|
+
centreClearances: parseIntOr0(cells[10] ?? "0"),
|
|
340
|
+
stoppageClearances: parseIntOr0(cells[11] ?? "0"),
|
|
341
|
+
scoreInvolvements: parseIntOr0(cells[12] ?? "0"),
|
|
342
|
+
metresGained: parseIntOr0(cells[13] ?? "0"),
|
|
343
|
+
turnovers: parseIntOr0(cells[14] ?? "0"),
|
|
344
|
+
intercepts: parseIntOr0(cells[15] ?? "0"),
|
|
345
|
+
tacklesInside50: parseIntOr0(cells[16] ?? "0"),
|
|
346
|
+
timeOnGroundPercentage: parseFloatOr0(cells[17] ?? "0")
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function parseBasicStats(html) {
|
|
350
|
+
return parseStatsTable(html, [...BASIC_COLS], parseBasicRow);
|
|
351
|
+
}
|
|
352
|
+
function parseAdvancedStats(html) {
|
|
353
|
+
return parseStatsTable(html, [...ADVANCED_COLS], parseAdvancedRow);
|
|
354
|
+
}
|
|
355
|
+
function mergeFootyWireStats(basicTeams, advancedTeams, matchId, season, roundNumber) {
|
|
356
|
+
const stats = [];
|
|
357
|
+
for (let teamIdx = 0; teamIdx < basicTeams.length; teamIdx++) {
|
|
358
|
+
const basicEntry = basicTeams[teamIdx];
|
|
359
|
+
const advancedEntry = advancedTeams[teamIdx];
|
|
360
|
+
if (!basicEntry) continue;
|
|
361
|
+
const [teamName, basicRows] = basicEntry;
|
|
362
|
+
const advancedRows = advancedEntry?.[1] ?? [];
|
|
363
|
+
const advancedByName = /* @__PURE__ */ new Map();
|
|
364
|
+
for (const adv of advancedRows) {
|
|
365
|
+
advancedByName.set(adv.player.toLowerCase(), adv);
|
|
366
|
+
}
|
|
367
|
+
for (const basic of basicRows) {
|
|
368
|
+
const nameParts = basic.player.split(/\s+/);
|
|
369
|
+
const surname = nameParts[nameParts.length - 1] ?? "";
|
|
370
|
+
const firstName = nameParts.slice(0, -1).join(" ");
|
|
371
|
+
const initial = firstName.charAt(0);
|
|
372
|
+
const abbrevName = `${initial} ${surname}`.toLowerCase();
|
|
373
|
+
const adv = advancedByName.get(abbrevName);
|
|
374
|
+
stats.push({
|
|
375
|
+
matchId: `FW_${matchId}`,
|
|
376
|
+
season,
|
|
377
|
+
roundNumber,
|
|
378
|
+
team: teamName,
|
|
379
|
+
competition: "AFLM",
|
|
380
|
+
playerId: `FW_${basic.player.replace(/\s+/g, "_")}`,
|
|
381
|
+
givenName: firstName,
|
|
382
|
+
surname,
|
|
383
|
+
displayName: basic.player,
|
|
384
|
+
jumperNumber: null,
|
|
385
|
+
kicks: basic.kicks,
|
|
386
|
+
handballs: basic.handballs,
|
|
387
|
+
disposals: basic.disposals,
|
|
388
|
+
marks: basic.marks,
|
|
389
|
+
goals: basic.goals,
|
|
390
|
+
behinds: basic.behinds,
|
|
391
|
+
tackles: basic.tackles,
|
|
392
|
+
hitouts: basic.hitouts,
|
|
393
|
+
freesFor: basic.freesFor,
|
|
394
|
+
freesAgainst: basic.freesAgainst,
|
|
395
|
+
contestedPossessions: adv?.contestedPossessions ?? null,
|
|
396
|
+
uncontestedPossessions: adv?.uncontestedPossessions ?? null,
|
|
397
|
+
contestedMarks: adv?.contestedMarks ?? null,
|
|
398
|
+
intercepts: adv?.intercepts ?? null,
|
|
399
|
+
centreClearances: adv?.centreClearances ?? null,
|
|
400
|
+
stoppageClearances: adv?.stoppageClearances ?? null,
|
|
401
|
+
totalClearances: basic.clearances,
|
|
402
|
+
inside50s: basic.inside50s,
|
|
403
|
+
rebound50s: basic.rebound50s,
|
|
404
|
+
clangers: basic.clangers,
|
|
405
|
+
turnovers: adv?.turnovers ?? null,
|
|
406
|
+
onePercenters: adv?.onePercenters ?? null,
|
|
407
|
+
bounces: adv?.bounces ?? null,
|
|
408
|
+
goalAssists: basic.goalAssists,
|
|
409
|
+
disposalEfficiency: adv?.disposalEfficiency ?? null,
|
|
410
|
+
metresGained: adv?.metresGained ?? null,
|
|
411
|
+
goalAccuracy: null,
|
|
412
|
+
marksInside50: adv?.marksInside50 ?? null,
|
|
413
|
+
tacklesInside50: adv?.tacklesInside50 ?? null,
|
|
414
|
+
shotsAtGoal: null,
|
|
415
|
+
scoreInvolvements: adv?.scoreInvolvements ?? null,
|
|
416
|
+
totalPossessions: null,
|
|
417
|
+
timeOnGroundPercentage: adv?.timeOnGroundPercentage ?? null,
|
|
418
|
+
ratingPoints: null,
|
|
419
|
+
dreamTeamPoints: basic.dreamTeamPoints,
|
|
420
|
+
effectiveDisposals: adv?.effectiveDisposals ?? null,
|
|
421
|
+
effectiveKicks: null,
|
|
422
|
+
kickEfficiency: null,
|
|
423
|
+
kickToHandballRatio: null,
|
|
424
|
+
pressureActs: null,
|
|
425
|
+
defHalfPressureActs: null,
|
|
426
|
+
spoils: null,
|
|
427
|
+
hitoutsToAdvantage: null,
|
|
428
|
+
hitoutWinPercentage: null,
|
|
429
|
+
hitoutToAdvantageRate: null,
|
|
430
|
+
groundBallGets: null,
|
|
431
|
+
f50GroundBallGets: null,
|
|
432
|
+
interceptMarks: null,
|
|
433
|
+
marksOnLead: null,
|
|
434
|
+
contestedPossessionRate: null,
|
|
435
|
+
contestOffOneOnOnes: null,
|
|
436
|
+
contestOffWins: null,
|
|
437
|
+
contestOffWinsPercentage: null,
|
|
438
|
+
contestDefOneOnOnes: null,
|
|
439
|
+
contestDefLosses: null,
|
|
440
|
+
contestDefLossPercentage: null,
|
|
441
|
+
centreBounceAttendances: null,
|
|
442
|
+
kickins: null,
|
|
443
|
+
kickinsPlayon: null,
|
|
444
|
+
ruckContests: null,
|
|
445
|
+
scoreLaunches: null,
|
|
446
|
+
source: "footywire"
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return stats;
|
|
451
|
+
}
|
|
452
|
+
var BASIC_COLS, ADVANCED_COLS;
|
|
453
|
+
var init_footywire_player_stats = __esm({
|
|
454
|
+
"src/transforms/footywire-player-stats.ts"() {
|
|
455
|
+
"use strict";
|
|
456
|
+
init_parse_utils();
|
|
457
|
+
BASIC_COLS = [
|
|
458
|
+
"Player",
|
|
459
|
+
"K",
|
|
460
|
+
"HB",
|
|
461
|
+
"D",
|
|
462
|
+
"M",
|
|
463
|
+
"G",
|
|
464
|
+
"B",
|
|
465
|
+
"T",
|
|
466
|
+
"HO",
|
|
467
|
+
"GA",
|
|
468
|
+
"I50",
|
|
469
|
+
"CL",
|
|
470
|
+
"CG",
|
|
471
|
+
"R50",
|
|
472
|
+
"FF",
|
|
473
|
+
"FA",
|
|
474
|
+
"AF",
|
|
475
|
+
"SC"
|
|
476
|
+
];
|
|
477
|
+
ADVANCED_COLS = [
|
|
478
|
+
"Player",
|
|
479
|
+
"CP",
|
|
480
|
+
"UP",
|
|
481
|
+
"ED",
|
|
482
|
+
"DE",
|
|
483
|
+
"CM",
|
|
484
|
+
"GA",
|
|
485
|
+
"MI5",
|
|
486
|
+
"1%",
|
|
487
|
+
"BO",
|
|
488
|
+
"CCL",
|
|
489
|
+
"SCL",
|
|
490
|
+
"SI",
|
|
491
|
+
"MG",
|
|
492
|
+
"TO",
|
|
493
|
+
"ITC",
|
|
494
|
+
"T5",
|
|
495
|
+
"TOG"
|
|
496
|
+
];
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// src/transforms/match-results.ts
|
|
501
|
+
function inferRoundType(roundName) {
|
|
502
|
+
return FINALS_PATTERN.test(roundName) ? "Finals" : "HomeAndAway";
|
|
503
|
+
}
|
|
504
|
+
function toMatchStatus(raw) {
|
|
505
|
+
switch (raw) {
|
|
506
|
+
case "CONCLUDED":
|
|
507
|
+
case "COMPLETE":
|
|
508
|
+
return "Complete";
|
|
509
|
+
case "LIVE":
|
|
510
|
+
case "IN_PROGRESS":
|
|
511
|
+
return "Live";
|
|
512
|
+
case "UPCOMING":
|
|
513
|
+
case "SCHEDULED":
|
|
514
|
+
return "Upcoming";
|
|
515
|
+
case "POSTPONED":
|
|
516
|
+
return "Postponed";
|
|
517
|
+
case "CANCELLED":
|
|
518
|
+
return "Cancelled";
|
|
519
|
+
default:
|
|
520
|
+
return "Complete";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function toQuarterScore(period) {
|
|
524
|
+
return {
|
|
525
|
+
goals: period.score.goals,
|
|
526
|
+
behinds: period.score.behinds,
|
|
527
|
+
points: period.score.totalScore
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function findPeriod(periods, quarter) {
|
|
531
|
+
if (!periods) return null;
|
|
532
|
+
const period = periods.find((p) => p.periodNumber === quarter);
|
|
533
|
+
return period ? toQuarterScore(period) : null;
|
|
534
|
+
}
|
|
535
|
+
function transformMatchItems(items, season, competition, source = "afl-api") {
|
|
536
|
+
return items.map((item) => {
|
|
537
|
+
const homeScore = item.score?.homeTeamScore;
|
|
538
|
+
const awayScore = item.score?.awayTeamScore;
|
|
539
|
+
const homePoints = homeScore?.matchScore.totalScore ?? 0;
|
|
540
|
+
const awayPoints = awayScore?.matchScore.totalScore ?? 0;
|
|
541
|
+
return {
|
|
542
|
+
matchId: item.match.matchId,
|
|
543
|
+
season,
|
|
544
|
+
roundNumber: item.round?.roundNumber ?? 0,
|
|
545
|
+
roundType: inferRoundType(item.round?.name ?? ""),
|
|
546
|
+
date: new Date(item.match.utcStartTime),
|
|
547
|
+
venue: item.venue?.name ?? "",
|
|
548
|
+
homeTeam: normaliseTeamName(item.match.homeTeam.name),
|
|
549
|
+
awayTeam: normaliseTeamName(item.match.awayTeam.name),
|
|
550
|
+
homeGoals: homeScore?.matchScore.goals ?? 0,
|
|
551
|
+
homeBehinds: homeScore?.matchScore.behinds ?? 0,
|
|
552
|
+
homePoints,
|
|
553
|
+
awayGoals: awayScore?.matchScore.goals ?? 0,
|
|
554
|
+
awayBehinds: awayScore?.matchScore.behinds ?? 0,
|
|
555
|
+
awayPoints,
|
|
556
|
+
margin: homePoints - awayPoints,
|
|
557
|
+
q1Home: findPeriod(homeScore?.periodScore, 1),
|
|
558
|
+
q2Home: findPeriod(homeScore?.periodScore, 2),
|
|
559
|
+
q3Home: findPeriod(homeScore?.periodScore, 3),
|
|
560
|
+
q4Home: findPeriod(homeScore?.periodScore, 4),
|
|
561
|
+
q1Away: findPeriod(awayScore?.periodScore, 1),
|
|
562
|
+
q2Away: findPeriod(awayScore?.periodScore, 2),
|
|
563
|
+
q3Away: findPeriod(awayScore?.periodScore, 3),
|
|
564
|
+
q4Away: findPeriod(awayScore?.periodScore, 4),
|
|
565
|
+
status: toMatchStatus(item.match.status),
|
|
566
|
+
attendance: null,
|
|
567
|
+
venueState: item.venue?.state ?? null,
|
|
568
|
+
venueTimezone: item.venue?.timeZone ?? null,
|
|
569
|
+
homeRushedBehinds: homeScore?.rushedBehinds ?? null,
|
|
570
|
+
awayRushedBehinds: awayScore?.rushedBehinds ?? null,
|
|
571
|
+
homeMinutesInFront: homeScore?.minutesInFront ?? null,
|
|
572
|
+
awayMinutesInFront: awayScore?.minutesInFront ?? null,
|
|
573
|
+
source,
|
|
574
|
+
competition
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
var FINALS_PATTERN;
|
|
579
|
+
var init_match_results = __esm({
|
|
580
|
+
"src/transforms/match-results.ts"() {
|
|
581
|
+
"use strict";
|
|
582
|
+
init_team_mapping();
|
|
583
|
+
FINALS_PATTERN = /final|elimination|qualifying|preliminary|semi|grand/i;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// src/sources/footywire.ts
|
|
588
|
+
import * as cheerio2 from "cheerio";
|
|
589
|
+
function parseMatchList(html, year) {
|
|
590
|
+
const $ = cheerio2.load(html);
|
|
591
|
+
const results = [];
|
|
592
|
+
let currentRound = 0;
|
|
593
|
+
let currentRoundType = "HomeAndAway";
|
|
594
|
+
$("tr").each((_i, row) => {
|
|
595
|
+
const roundHeader = $(row).find("td[colspan='7']");
|
|
596
|
+
if (roundHeader.length > 0) {
|
|
597
|
+
const text = roundHeader.text().trim();
|
|
598
|
+
currentRoundType = inferRoundType(text);
|
|
599
|
+
const roundMatch = /Round\s+(\d+)/i.exec(text);
|
|
600
|
+
if (roundMatch?.[1]) {
|
|
601
|
+
currentRound = Number.parseInt(roundMatch[1], 10);
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const cells = $(row).find("td.data");
|
|
606
|
+
if (cells.length < 5) return;
|
|
607
|
+
const dateText = $(cells[0]).text().trim();
|
|
608
|
+
const teamsCell = $(cells[1]);
|
|
609
|
+
const venue = $(cells[2]).text().trim();
|
|
610
|
+
const attendance = $(cells[3]).text().trim();
|
|
611
|
+
const scoreCell = $(cells[4]);
|
|
612
|
+
if (venue === "BYE") return;
|
|
613
|
+
const teamLinks = teamsCell.find("a");
|
|
614
|
+
if (teamLinks.length < 2) return;
|
|
615
|
+
const homeTeam = normaliseTeamName($(teamLinks[0]).text().trim());
|
|
616
|
+
const awayTeam = normaliseTeamName($(teamLinks[1]).text().trim());
|
|
617
|
+
const scoreText = scoreCell.text().trim();
|
|
618
|
+
const scoreMatch = /(\d+)-(\d+)/.exec(scoreText);
|
|
619
|
+
if (!scoreMatch) return;
|
|
620
|
+
const homePoints = Number.parseInt(scoreMatch[1] ?? "0", 10);
|
|
621
|
+
const awayPoints = Number.parseInt(scoreMatch[2] ?? "0", 10);
|
|
622
|
+
const scoreLink = scoreCell.find("a").attr("href") ?? "";
|
|
623
|
+
const midMatch = /mid=(\d+)/.exec(scoreLink);
|
|
624
|
+
const matchId = midMatch?.[1] ? `FW_${midMatch[1]}` : `FW_${year}_R${currentRound}_${homeTeam}`;
|
|
625
|
+
const date = parseFootyWireDate(dateText) ?? new Date(year, 0, 1);
|
|
626
|
+
const homeGoals = Math.floor(homePoints / 6);
|
|
627
|
+
const homeBehinds = homePoints - homeGoals * 6;
|
|
628
|
+
const awayGoals = Math.floor(awayPoints / 6);
|
|
629
|
+
const awayBehinds = awayPoints - awayGoals * 6;
|
|
630
|
+
results.push({
|
|
631
|
+
matchId,
|
|
632
|
+
season: year,
|
|
633
|
+
roundNumber: currentRound,
|
|
634
|
+
roundType: currentRoundType,
|
|
635
|
+
date,
|
|
636
|
+
venue,
|
|
637
|
+
homeTeam,
|
|
638
|
+
awayTeam,
|
|
639
|
+
homeGoals,
|
|
640
|
+
homeBehinds,
|
|
641
|
+
homePoints,
|
|
642
|
+
awayGoals,
|
|
643
|
+
awayBehinds,
|
|
644
|
+
awayPoints,
|
|
645
|
+
margin: homePoints - awayPoints,
|
|
646
|
+
q1Home: null,
|
|
647
|
+
q2Home: null,
|
|
648
|
+
q3Home: null,
|
|
649
|
+
q4Home: null,
|
|
650
|
+
q1Away: null,
|
|
651
|
+
q2Away: null,
|
|
652
|
+
q3Away: null,
|
|
653
|
+
q4Away: null,
|
|
654
|
+
status: "Complete",
|
|
655
|
+
attendance: attendance ? Number.parseInt(attendance, 10) || null : null,
|
|
656
|
+
venueState: null,
|
|
657
|
+
venueTimezone: null,
|
|
658
|
+
homeRushedBehinds: null,
|
|
659
|
+
awayRushedBehinds: null,
|
|
660
|
+
homeMinutesInFront: null,
|
|
661
|
+
awayMinutesInFront: null,
|
|
662
|
+
source: "footywire",
|
|
663
|
+
competition: "AFLM"
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
return results;
|
|
667
|
+
}
|
|
668
|
+
function parseFixtureList(html, year) {
|
|
669
|
+
const $ = cheerio2.load(html);
|
|
670
|
+
const fixtures = [];
|
|
671
|
+
let currentRound = 0;
|
|
672
|
+
let currentRoundType = "HomeAndAway";
|
|
673
|
+
let gameNumber = 0;
|
|
674
|
+
$("tr").each((_i, row) => {
|
|
675
|
+
const roundHeader = $(row).find("td[colspan='7']");
|
|
676
|
+
if (roundHeader.length > 0) {
|
|
677
|
+
const text = roundHeader.text().trim();
|
|
678
|
+
currentRoundType = inferRoundType(text);
|
|
679
|
+
const roundMatch = /Round\s+(\d+)/i.exec(text);
|
|
680
|
+
if (roundMatch?.[1]) {
|
|
681
|
+
currentRound = Number.parseInt(roundMatch[1], 10);
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const cells = $(row).find("td.data");
|
|
686
|
+
if (cells.length < 3) return;
|
|
687
|
+
const dateText = $(cells[0]).text().trim();
|
|
688
|
+
const teamsCell = $(cells[1]);
|
|
689
|
+
const venue = $(cells[2]).text().trim();
|
|
690
|
+
if (venue === "BYE") return;
|
|
691
|
+
const teamLinks = teamsCell.find("a");
|
|
692
|
+
if (teamLinks.length < 2) return;
|
|
693
|
+
const homeTeam = normaliseTeamName($(teamLinks[0]).text().trim());
|
|
694
|
+
const awayTeam = normaliseTeamName($(teamLinks[1]).text().trim());
|
|
695
|
+
const date = parseFootyWireDate(dateText) ?? new Date(year, 0, 1);
|
|
696
|
+
gameNumber++;
|
|
697
|
+
const scoreCell = cells.length >= 5 ? $(cells[4]) : null;
|
|
698
|
+
const scoreText = scoreCell?.text().trim() ?? "";
|
|
699
|
+
const hasScore = /\d+-\d+/.test(scoreText);
|
|
700
|
+
fixtures.push({
|
|
701
|
+
matchId: `FW_${year}_R${currentRound}_G${gameNumber}`,
|
|
702
|
+
season: year,
|
|
703
|
+
roundNumber: currentRound,
|
|
704
|
+
roundType: currentRoundType,
|
|
705
|
+
date,
|
|
706
|
+
venue,
|
|
707
|
+
homeTeam,
|
|
708
|
+
awayTeam,
|
|
709
|
+
status: hasScore ? "Complete" : "Upcoming",
|
|
710
|
+
competition: "AFLM"
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
return fixtures;
|
|
714
|
+
}
|
|
715
|
+
function parseFootyWireTeamStats(html, year, suffix) {
|
|
716
|
+
const $ = cheerio2.load(html);
|
|
717
|
+
const entries = [];
|
|
718
|
+
const tables = $("table");
|
|
719
|
+
const mainTable = tables.length > 10 ? $(tables[10]) : $("table.sortable").first();
|
|
720
|
+
if (mainTable.length === 0) return entries;
|
|
721
|
+
const STAT_KEYS = [
|
|
722
|
+
"K",
|
|
723
|
+
"HB",
|
|
724
|
+
"D",
|
|
725
|
+
"M",
|
|
726
|
+
"G",
|
|
727
|
+
"GA",
|
|
728
|
+
"I50",
|
|
729
|
+
"BH",
|
|
730
|
+
"T",
|
|
731
|
+
"HO",
|
|
732
|
+
"FF",
|
|
733
|
+
"FA",
|
|
734
|
+
"CL",
|
|
735
|
+
"CG",
|
|
736
|
+
"R50",
|
|
737
|
+
"AF",
|
|
738
|
+
"SC"
|
|
739
|
+
];
|
|
740
|
+
const rows = mainTable.find("tr");
|
|
741
|
+
rows.each((rowIdx, row) => {
|
|
742
|
+
if (rowIdx === 0) return;
|
|
743
|
+
const cells = $(row).find("td");
|
|
744
|
+
if (cells.length < 20) return;
|
|
745
|
+
const teamLink = $(cells[1]).find("a");
|
|
746
|
+
const teamText = teamLink.length > 0 ? teamLink.text().trim() : $(cells[1]).text().trim();
|
|
747
|
+
const teamName = normaliseTeamName(teamText);
|
|
748
|
+
if (!teamName) return;
|
|
749
|
+
const parseNum = (cell) => Number.parseFloat(cell.text().trim()) || 0;
|
|
750
|
+
const gamesPlayed = parseNum($(cells[2]));
|
|
751
|
+
const stats = {};
|
|
752
|
+
for (let i = 0; i < STAT_KEYS.length; i++) {
|
|
753
|
+
const key = suffix === "against" ? `${STAT_KEYS[i]}_against` : STAT_KEYS[i];
|
|
754
|
+
stats[key] = parseNum($(cells[i + 3]));
|
|
755
|
+
}
|
|
756
|
+
entries.push({
|
|
757
|
+
season: year,
|
|
758
|
+
team: teamName,
|
|
759
|
+
gamesPlayed,
|
|
760
|
+
stats,
|
|
761
|
+
source: "footywire"
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
return entries;
|
|
765
|
+
}
|
|
766
|
+
function mergeTeamAndOppStats(teamStats, oppStats) {
|
|
767
|
+
const oppMap = /* @__PURE__ */ new Map();
|
|
768
|
+
for (const entry of oppStats) {
|
|
769
|
+
oppMap.set(entry.team, entry.stats);
|
|
770
|
+
}
|
|
771
|
+
return teamStats.map((entry) => {
|
|
772
|
+
const opp = oppMap.get(entry.team);
|
|
773
|
+
if (!opp) return entry;
|
|
774
|
+
return {
|
|
775
|
+
...entry,
|
|
776
|
+
stats: { ...entry.stats, ...opp }
|
|
777
|
+
};
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
function teamNameToFootyWireSlug(teamName) {
|
|
781
|
+
return FOOTYWIRE_SLUG_MAP.get(teamName);
|
|
782
|
+
}
|
|
783
|
+
function parseFootyWirePlayerList(html, teamName) {
|
|
784
|
+
const $ = cheerio2.load(html);
|
|
785
|
+
const players = [];
|
|
786
|
+
let dataRows = null;
|
|
787
|
+
$("table").each((_i, table) => {
|
|
788
|
+
const firstRow = $(table).find("tr").first();
|
|
789
|
+
const cells = firstRow.find("td, th");
|
|
790
|
+
const cellTexts = cells.map((_j, c) => $(c).text().trim()).get();
|
|
791
|
+
if (cellTexts.includes("Age") && cellTexts.includes("Name")) {
|
|
792
|
+
dataRows = $(table).find("tr");
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
if (!dataRows) return players;
|
|
797
|
+
dataRows.each((_rowIdx, row) => {
|
|
798
|
+
const cells = $(row).find("td");
|
|
799
|
+
if (cells.length < 6) return;
|
|
800
|
+
const jumperText = $(cells[0]).text().trim();
|
|
801
|
+
const nameText = $(cells[1]).text().trim();
|
|
802
|
+
const gamesText = $(cells[2]).text().trim();
|
|
803
|
+
const dobText = cells.length > 4 ? $(cells[4]).text().trim() : "";
|
|
804
|
+
const heightText = cells.length > 5 ? $(cells[5]).text().trim() : "";
|
|
805
|
+
const origin = cells.length > 6 ? $(cells[6]).text().trim() : "";
|
|
806
|
+
const position = cells.length > 7 ? $(cells[7]).text().trim() : "";
|
|
807
|
+
const cleanedName = nameText.replace(/\nR$/, "").trim();
|
|
808
|
+
if (!cleanedName || cleanedName === "Name") return;
|
|
809
|
+
const nameParts = cleanedName.split(",").map((s) => s.trim());
|
|
810
|
+
const surname = nameParts[0] ?? "";
|
|
811
|
+
const givenName = nameParts[1] ?? "";
|
|
812
|
+
const jumperNumber = jumperText ? Number.parseInt(jumperText, 10) || null : null;
|
|
813
|
+
const gamesPlayed = gamesText ? Number.parseInt(gamesText, 10) || null : null;
|
|
814
|
+
const heightMatch = /(\d+)cm/.exec(heightText);
|
|
815
|
+
const heightCm = heightMatch?.[1] ? Number.parseInt(heightMatch[1], 10) || null : null;
|
|
816
|
+
players.push({
|
|
817
|
+
playerId: `FW_${teamName}_${surname}_${givenName}`.replace(/\s+/g, "_"),
|
|
818
|
+
givenName,
|
|
819
|
+
surname,
|
|
820
|
+
displayName: givenName ? `${givenName} ${surname}` : surname,
|
|
821
|
+
team: teamName,
|
|
822
|
+
jumperNumber,
|
|
823
|
+
position: position || null,
|
|
824
|
+
dateOfBirth: dobText || null,
|
|
825
|
+
heightCm,
|
|
826
|
+
weightKg: null,
|
|
827
|
+
gamesPlayed,
|
|
828
|
+
goals: null,
|
|
829
|
+
draftYear: null,
|
|
830
|
+
draftPosition: null,
|
|
831
|
+
draftType: null,
|
|
832
|
+
debutYear: null,
|
|
833
|
+
recruitedFrom: origin || null
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
return players;
|
|
837
|
+
}
|
|
838
|
+
var FOOTYWIRE_BASE, FootyWireClient, FOOTYWIRE_SLUG_MAP;
|
|
839
|
+
var init_footywire = __esm({
|
|
840
|
+
"src/sources/footywire.ts"() {
|
|
841
|
+
"use strict";
|
|
842
|
+
init_date_utils();
|
|
843
|
+
init_errors();
|
|
844
|
+
init_result();
|
|
845
|
+
init_team_mapping();
|
|
846
|
+
init_footywire_player_stats();
|
|
847
|
+
init_match_results();
|
|
848
|
+
FOOTYWIRE_BASE = "https://www.footywire.com/afl/footy";
|
|
849
|
+
FootyWireClient = class {
|
|
850
|
+
fetchFn;
|
|
851
|
+
constructor(options) {
|
|
852
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Fetch the HTML content of any URL using this client's fetch function.
|
|
856
|
+
*
|
|
857
|
+
* Public wrapper around the internal fetchHtml for use by external modules
|
|
858
|
+
* (e.g. awards) that need to scrape FootyWire pages.
|
|
859
|
+
*/
|
|
860
|
+
async fetchPage(url) {
|
|
861
|
+
return this.fetchHtml(url);
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Fetch the HTML content of a FootyWire page.
|
|
865
|
+
*/
|
|
866
|
+
async fetchHtml(url) {
|
|
867
|
+
try {
|
|
868
|
+
const response = await this.fetchFn(url, {
|
|
869
|
+
headers: {
|
|
870
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
if (!response.ok) {
|
|
874
|
+
return err(
|
|
875
|
+
new ScrapeError(`FootyWire request failed: ${response.status} (${url})`, "footywire")
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
const html = await response.text();
|
|
879
|
+
return ok(html);
|
|
880
|
+
} catch (cause) {
|
|
881
|
+
return err(
|
|
882
|
+
new ScrapeError(
|
|
883
|
+
`FootyWire request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
884
|
+
"footywire"
|
|
885
|
+
)
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Fetch season match results from FootyWire.
|
|
891
|
+
*
|
|
892
|
+
* @param year - The season year.
|
|
893
|
+
* @returns Array of match results.
|
|
894
|
+
*/
|
|
895
|
+
async fetchSeasonResults(year) {
|
|
896
|
+
const url = `${FOOTYWIRE_BASE}/ft_match_list?year=${year}`;
|
|
897
|
+
const htmlResult = await this.fetchHtml(url);
|
|
898
|
+
if (!htmlResult.success) {
|
|
899
|
+
return htmlResult;
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
const results = parseMatchList(htmlResult.data, year);
|
|
903
|
+
return ok(results);
|
|
904
|
+
} catch (cause) {
|
|
905
|
+
return err(
|
|
906
|
+
new ScrapeError(
|
|
907
|
+
`Failed to parse match list: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
908
|
+
"footywire"
|
|
909
|
+
)
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Fetch player statistics for a single match.
|
|
915
|
+
*
|
|
916
|
+
* Scrapes both the basic and advanced stats pages.
|
|
917
|
+
* Available from 2010 onwards.
|
|
918
|
+
*
|
|
919
|
+
* @param matchId - The FootyWire match ID (numeric string).
|
|
920
|
+
* @param season - The season year.
|
|
921
|
+
* @param roundNumber - The round number.
|
|
922
|
+
*/
|
|
923
|
+
async fetchMatchPlayerStats(matchId, season, roundNumber) {
|
|
924
|
+
const basicUrl = `${FOOTYWIRE_BASE}/ft_match_statistics?mid=${matchId}`;
|
|
925
|
+
const advancedUrl = `${FOOTYWIRE_BASE}/ft_match_statistics?mid=${matchId}&advv=Y`;
|
|
926
|
+
const basicResult = await this.fetchHtml(basicUrl);
|
|
927
|
+
if (!basicResult.success) return basicResult;
|
|
928
|
+
const advancedResult = await this.fetchHtml(advancedUrl);
|
|
929
|
+
if (!advancedResult.success) return advancedResult;
|
|
930
|
+
try {
|
|
931
|
+
const basicTeams = parseBasicStats(basicResult.data);
|
|
932
|
+
const advancedTeams = parseAdvancedStats(advancedResult.data);
|
|
933
|
+
const stats = mergeFootyWireStats(basicTeams, advancedTeams, matchId, season, roundNumber);
|
|
934
|
+
return ok(stats);
|
|
935
|
+
} catch (cause) {
|
|
936
|
+
return err(
|
|
937
|
+
new ScrapeError(
|
|
938
|
+
`Failed to parse match stats: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
939
|
+
"footywire"
|
|
940
|
+
)
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Fetch match IDs from a season's match list page.
|
|
946
|
+
*
|
|
947
|
+
* Extracts `mid=XXXX` values from score links.
|
|
948
|
+
*
|
|
949
|
+
* @param year - The season year.
|
|
950
|
+
* @returns Array of match ID strings.
|
|
951
|
+
*/
|
|
952
|
+
async fetchSeasonMatchIds(year) {
|
|
953
|
+
const url = `${FOOTYWIRE_BASE}/ft_match_list?year=${year}`;
|
|
954
|
+
const htmlResult = await this.fetchHtml(url);
|
|
955
|
+
if (!htmlResult.success) return htmlResult;
|
|
956
|
+
try {
|
|
957
|
+
const $ = cheerio2.load(htmlResult.data);
|
|
958
|
+
const ids = [];
|
|
959
|
+
$(".data:nth-child(5) a").each((_i, el) => {
|
|
960
|
+
const href = $(el).attr("href") ?? "";
|
|
961
|
+
const match = /mid=(\d+)/.exec(href);
|
|
962
|
+
if (match?.[1]) {
|
|
963
|
+
ids.push(match[1]);
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
return ok(ids);
|
|
967
|
+
} catch (cause) {
|
|
968
|
+
return err(
|
|
969
|
+
new ScrapeError(
|
|
970
|
+
`Failed to parse match IDs: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
971
|
+
"footywire"
|
|
972
|
+
)
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Fetch player list (team history) from FootyWire.
|
|
978
|
+
*
|
|
979
|
+
* Scrapes the team history page (e.g. `th-hawthorn-hawks`) which lists
|
|
980
|
+
* all players who have played for that team.
|
|
981
|
+
*
|
|
982
|
+
* @param teamName - Canonical team name (e.g. "Hawthorn").
|
|
983
|
+
* @returns Array of player details (without source/competition fields).
|
|
984
|
+
*/
|
|
985
|
+
async fetchPlayerList(teamName) {
|
|
986
|
+
const slug = teamNameToFootyWireSlug(teamName);
|
|
987
|
+
if (!slug) {
|
|
988
|
+
return err(new ScrapeError(`No FootyWire slug mapping for team: ${teamName}`, "footywire"));
|
|
989
|
+
}
|
|
990
|
+
const url = `${FOOTYWIRE_BASE}/tp-${slug}`;
|
|
991
|
+
const htmlResult = await this.fetchHtml(url);
|
|
992
|
+
if (!htmlResult.success) return htmlResult;
|
|
993
|
+
try {
|
|
994
|
+
const players = parseFootyWirePlayerList(htmlResult.data, teamName);
|
|
995
|
+
return ok(players);
|
|
996
|
+
} catch (cause) {
|
|
997
|
+
return err(
|
|
998
|
+
new ScrapeError(
|
|
999
|
+
`Failed to parse player list: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1000
|
+
"footywire"
|
|
1001
|
+
)
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Fetch fixture data from FootyWire.
|
|
1007
|
+
*
|
|
1008
|
+
* Parses the match list page to extract scheduled matches with dates and venues.
|
|
1009
|
+
*
|
|
1010
|
+
* @param year - The season year.
|
|
1011
|
+
* @returns Array of fixture entries.
|
|
1012
|
+
*/
|
|
1013
|
+
async fetchSeasonFixture(year) {
|
|
1014
|
+
const url = `${FOOTYWIRE_BASE}/ft_match_list?year=${year}`;
|
|
1015
|
+
const htmlResult = await this.fetchHtml(url);
|
|
1016
|
+
if (!htmlResult.success) return htmlResult;
|
|
1017
|
+
try {
|
|
1018
|
+
const fixtures = parseFixtureList(htmlResult.data, year);
|
|
1019
|
+
return ok(fixtures);
|
|
1020
|
+
} catch (cause) {
|
|
1021
|
+
return err(
|
|
1022
|
+
new ScrapeError(
|
|
1023
|
+
`Failed to parse fixture list: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1024
|
+
"footywire"
|
|
1025
|
+
)
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Fetch team statistics from FootyWire.
|
|
1031
|
+
*
|
|
1032
|
+
* Scrapes team-level aggregate stats (totals or averages) for a season.
|
|
1033
|
+
*
|
|
1034
|
+
* @param year - The season year.
|
|
1035
|
+
* @param summaryType - "totals" or "averages" (default "totals").
|
|
1036
|
+
* @returns Array of team stats entries.
|
|
1037
|
+
*/
|
|
1038
|
+
async fetchTeamStats(year, summaryType = "totals") {
|
|
1039
|
+
const teamType = summaryType === "averages" ? "TA" : "TT";
|
|
1040
|
+
const oppType = summaryType === "averages" ? "OA" : "OT";
|
|
1041
|
+
const teamUrl = `${FOOTYWIRE_BASE}/ft_team_rankings?year=${year}&type=${teamType}&sby=2`;
|
|
1042
|
+
const oppUrl = `${FOOTYWIRE_BASE}/ft_team_rankings?year=${year}&type=${oppType}&sby=2`;
|
|
1043
|
+
const [teamResult, oppResult] = await Promise.all([
|
|
1044
|
+
this.fetchHtml(teamUrl),
|
|
1045
|
+
this.fetchHtml(oppUrl)
|
|
1046
|
+
]);
|
|
1047
|
+
if (!teamResult.success) return teamResult;
|
|
1048
|
+
if (!oppResult.success) return oppResult;
|
|
1049
|
+
try {
|
|
1050
|
+
const teamStats = parseFootyWireTeamStats(teamResult.data, year, "for");
|
|
1051
|
+
const oppStats = parseFootyWireTeamStats(oppResult.data, year, "against");
|
|
1052
|
+
const merged = mergeTeamAndOppStats(teamStats, oppStats);
|
|
1053
|
+
return ok(merged);
|
|
1054
|
+
} catch (cause) {
|
|
1055
|
+
return err(
|
|
1056
|
+
new ScrapeError(
|
|
1057
|
+
`Failed to parse team stats: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1058
|
+
"footywire"
|
|
1059
|
+
)
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
FOOTYWIRE_SLUG_MAP = /* @__PURE__ */ new Map([
|
|
1065
|
+
["Adelaide Crows", "adelaide-crows"],
|
|
1066
|
+
["Brisbane Lions", "brisbane-lions"],
|
|
1067
|
+
["Carlton", "carlton-blues"],
|
|
1068
|
+
["Collingwood", "collingwood-magpies"],
|
|
1069
|
+
["Essendon", "essendon-bombers"],
|
|
1070
|
+
["Fremantle", "fremantle-dockers"],
|
|
1071
|
+
["Geelong Cats", "geelong-cats"],
|
|
1072
|
+
["Gold Coast Suns", "gold-coast-suns"],
|
|
1073
|
+
["GWS Giants", "greater-western-sydney-giants"],
|
|
1074
|
+
["Hawthorn", "hawthorn-hawks"],
|
|
1075
|
+
["Melbourne", "melbourne-demons"],
|
|
1076
|
+
["North Melbourne", "north-melbourne-kangaroos"],
|
|
1077
|
+
["Port Adelaide", "port-adelaide-power"],
|
|
1078
|
+
["Richmond", "richmond-tigers"],
|
|
1079
|
+
["St Kilda", "st-kilda-saints"],
|
|
1080
|
+
["Sydney Swans", "sydney-swans"],
|
|
1081
|
+
["West Coast Eagles", "west-coast-eagles"],
|
|
1082
|
+
["Western Bulldogs", "western-bulldogs"]
|
|
1083
|
+
]);
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
// src/sources/afl-coaches.ts
|
|
1088
|
+
import * as cheerio3 from "cheerio";
|
|
1089
|
+
function parseCoachesVotesHtml(html, season, roundNumber) {
|
|
1090
|
+
const $ = cheerio3.load(html);
|
|
1091
|
+
const clubLogos = $(".pr-md-3.votes-by-match .club_logo");
|
|
1092
|
+
const homeTeams = [];
|
|
1093
|
+
const awayTeams = [];
|
|
1094
|
+
clubLogos.each((i, el) => {
|
|
1095
|
+
const title = $(el).attr("title") ?? "";
|
|
1096
|
+
if (i % 2 === 0) {
|
|
1097
|
+
homeTeams.push(title);
|
|
1098
|
+
} else {
|
|
1099
|
+
awayTeams.push(title);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
const rawVotes = [];
|
|
1103
|
+
$(".pr-md-3.votes-by-match .col-2").each((_i, el) => {
|
|
1104
|
+
const text = $(el).text().replace(/\n/g, "").replace(/\t/g, "").trim();
|
|
1105
|
+
rawVotes.push(text);
|
|
1106
|
+
});
|
|
1107
|
+
const rawPlayers = [];
|
|
1108
|
+
$(".pr-md-3.votes-by-match .col-10").each((_i, el) => {
|
|
1109
|
+
const text = $(el).text().replace(/\n/g, "").replace(/\t/g, "").trim();
|
|
1110
|
+
rawPlayers.push(text);
|
|
1111
|
+
});
|
|
1112
|
+
const votes = [];
|
|
1113
|
+
let matchIndex = 0;
|
|
1114
|
+
for (let i = 0; i < rawPlayers.length; i++) {
|
|
1115
|
+
const playerName = rawPlayers[i] ?? "";
|
|
1116
|
+
const voteText = rawVotes[i] ?? "";
|
|
1117
|
+
if (playerName === "Player (Club)" && voteText === "Votes") {
|
|
1118
|
+
matchIndex++;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
const homeTeam = homeTeams[matchIndex - 1];
|
|
1122
|
+
const awayTeam = awayTeams[matchIndex - 1];
|
|
1123
|
+
if (homeTeam == null || awayTeam == null) {
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const voteCount = Number(voteText);
|
|
1127
|
+
if (Number.isNaN(voteCount)) {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
votes.push({
|
|
1131
|
+
season,
|
|
1132
|
+
round: roundNumber,
|
|
1133
|
+
homeTeam,
|
|
1134
|
+
awayTeam,
|
|
1135
|
+
playerName,
|
|
1136
|
+
votes: voteCount
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
return votes;
|
|
1140
|
+
}
|
|
1141
|
+
var AflCoachesClient;
|
|
1142
|
+
var init_afl_coaches = __esm({
|
|
1143
|
+
"src/sources/afl-coaches.ts"() {
|
|
1144
|
+
"use strict";
|
|
1145
|
+
init_errors();
|
|
1146
|
+
init_result();
|
|
1147
|
+
AflCoachesClient = class {
|
|
1148
|
+
fetchFn;
|
|
1149
|
+
constructor(options) {
|
|
1150
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Fetch the HTML content of an AFLCA page.
|
|
1154
|
+
*/
|
|
1155
|
+
async fetchHtml(url) {
|
|
1156
|
+
try {
|
|
1157
|
+
const response = await this.fetchFn(url, {
|
|
1158
|
+
headers: {
|
|
1159
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
if (!response.ok) {
|
|
1163
|
+
return err(
|
|
1164
|
+
new ScrapeError(`AFL Coaches request failed: ${response.status} (${url})`, "afl-coaches")
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
const html = await response.text();
|
|
1168
|
+
return ok(html);
|
|
1169
|
+
} catch (cause) {
|
|
1170
|
+
return err(
|
|
1171
|
+
new ScrapeError(
|
|
1172
|
+
`AFL Coaches request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1173
|
+
"afl-coaches"
|
|
1174
|
+
)
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Build the AFLCA leaderboard URL for a given season, round, and competition.
|
|
1180
|
+
*
|
|
1181
|
+
* Mirrors the R package URL construction from `helper-aflcoaches.R`.
|
|
1182
|
+
*
|
|
1183
|
+
* @param season - Season year (e.g. 2024).
|
|
1184
|
+
* @param roundNumber - Round number.
|
|
1185
|
+
* @param competition - "AFLM" or "AFLW".
|
|
1186
|
+
* @param isFinals - Whether this is a finals round.
|
|
1187
|
+
*/
|
|
1188
|
+
buildUrl(season, roundNumber, competition, isFinals) {
|
|
1189
|
+
const linkBase = competition === "AFLW" ? "https://aflcoaches.com.au/awards/aflw-champion-player-of-the-year-award/leaderboard/" : isFinals ? "https://aflcoaches.com.au/awards/gary-ayres-award-best-finals-player/leaderboard/" : "https://aflcoaches.com.au/awards/the-aflca-champion-player-of-the-year-award/leaderboard/";
|
|
1190
|
+
const compSuffix = competition === "AFLW" ? "02" : "01";
|
|
1191
|
+
const secondPart = season >= 2023 ? season + 1 : season;
|
|
1192
|
+
const roundPad = String(roundNumber).padStart(2, "0");
|
|
1193
|
+
return `${linkBase}${season}/${secondPart}${compSuffix}${roundPad}`;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Scrape coaches votes for a single round.
|
|
1197
|
+
*
|
|
1198
|
+
* @param season - Season year.
|
|
1199
|
+
* @param roundNumber - Round number.
|
|
1200
|
+
* @param competition - "AFLM" or "AFLW".
|
|
1201
|
+
* @param isFinals - Whether this is a finals round.
|
|
1202
|
+
* @returns Array of coaches vote records for that round.
|
|
1203
|
+
*/
|
|
1204
|
+
async scrapeRoundVotes(season, roundNumber, competition, isFinals) {
|
|
1205
|
+
const url = this.buildUrl(season, roundNumber, competition, isFinals);
|
|
1206
|
+
const htmlResult = await this.fetchHtml(url);
|
|
1207
|
+
if (!htmlResult.success) {
|
|
1208
|
+
return htmlResult;
|
|
1209
|
+
}
|
|
1210
|
+
try {
|
|
1211
|
+
const votes = parseCoachesVotesHtml(htmlResult.data, season, roundNumber);
|
|
1212
|
+
return ok(votes);
|
|
1213
|
+
} catch (cause) {
|
|
1214
|
+
return err(
|
|
1215
|
+
new ScrapeError(
|
|
1216
|
+
`Failed to parse coaches votes: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1217
|
+
"afl-coaches"
|
|
1218
|
+
)
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Fetch coaches votes for an entire season (all rounds).
|
|
1224
|
+
*
|
|
1225
|
+
* Iterates over rounds 1-30, skipping rounds that return errors (e.g. byes or
|
|
1226
|
+
* rounds that haven't been played yet). Finals rounds (>= 24) use the finals URL.
|
|
1227
|
+
*
|
|
1228
|
+
* @param season - Season year.
|
|
1229
|
+
* @param competition - "AFLM" or "AFLW".
|
|
1230
|
+
* @returns Combined array of coaches votes for the season.
|
|
1231
|
+
*/
|
|
1232
|
+
async fetchSeasonVotes(season, competition) {
|
|
1233
|
+
const allVotes = [];
|
|
1234
|
+
const maxRound = 30;
|
|
1235
|
+
for (let round = 1; round <= maxRound; round++) {
|
|
1236
|
+
const isFinals = round >= 24 && season >= 2018;
|
|
1237
|
+
const result = await this.scrapeRoundVotes(season, round, competition, isFinals);
|
|
1238
|
+
if (result.success && result.data.length > 0) {
|
|
1239
|
+
allVotes.push(...result.data);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (allVotes.length === 0) {
|
|
1243
|
+
return err(new ScrapeError(`No coaches votes found for season ${season}`, "afl-coaches"));
|
|
1244
|
+
}
|
|
1245
|
+
return ok(allVotes);
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// src/api/coaches-votes.ts
|
|
1252
|
+
async function fetchCoachesVotes(query) {
|
|
1253
|
+
const competition = query.competition ?? "AFLM";
|
|
1254
|
+
if (query.season < 2006) {
|
|
1255
|
+
return err(new ScrapeError("No coaches votes data available before 2006", "afl-coaches"));
|
|
1256
|
+
}
|
|
1257
|
+
if (competition === "AFLW" && query.season < 2018) {
|
|
1258
|
+
return err(new ScrapeError("No AFLW coaches votes data available before 2018", "afl-coaches"));
|
|
1259
|
+
}
|
|
1260
|
+
const client = new AflCoachesClient();
|
|
1261
|
+
let result;
|
|
1262
|
+
if (query.round != null) {
|
|
1263
|
+
const isFinals = query.round >= 24 && query.season >= 2018;
|
|
1264
|
+
result = await client.scrapeRoundVotes(query.season, query.round, competition, isFinals);
|
|
1265
|
+
} else {
|
|
1266
|
+
result = await client.fetchSeasonVotes(query.season, competition);
|
|
1267
|
+
}
|
|
1268
|
+
if (!result.success) {
|
|
1269
|
+
return result;
|
|
1270
|
+
}
|
|
1271
|
+
let votes = result.data;
|
|
1272
|
+
if (query.team != null) {
|
|
1273
|
+
const normalisedTeam = normaliseTeamName(query.team);
|
|
1274
|
+
votes = votes.filter(
|
|
1275
|
+
(v) => normaliseTeamName(v.homeTeam) === normalisedTeam || normaliseTeamName(v.awayTeam) === normalisedTeam
|
|
1276
|
+
);
|
|
1277
|
+
if (votes.length === 0) {
|
|
1278
|
+
return ok([]);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return ok(votes);
|
|
1282
|
+
}
|
|
1283
|
+
var init_coaches_votes = __esm({
|
|
1284
|
+
"src/api/coaches-votes.ts"() {
|
|
1285
|
+
"use strict";
|
|
1286
|
+
init_errors();
|
|
1287
|
+
init_result();
|
|
1288
|
+
init_team_mapping();
|
|
1289
|
+
init_afl_coaches();
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
// src/lib/validation.ts
|
|
1294
|
+
import { z } from "zod/v4";
|
|
1295
|
+
var AflApiTokenSchema, CompetitionSchema, CompetitionListSchema, CompseasonSchema, CompseasonListSchema, RoundSchema, RoundListSchema, ScoreSchema, PeriodScoreSchema, TeamScoreSchema, CfsMatchTeamSchema, CfsMatchSchema, CfsScoreSchema, CfsVenueSchema, MatchItemSchema, MatchItemListSchema, CfsPlayerInnerSchema, PlayerGameStatsSchema, PlayerStatsItemSchema, PlayerStatsListSchema, RosterPlayerSchema, TeamPlayersSchema, MatchRosterSchema, TeamItemSchema, TeamListSchema, SquadPlayerInnerSchema, SquadPlayerItemSchema, SquadSchema, SquadListSchema, WinLossRecordSchema, LadderEntryRawSchema, LadderResponseSchema;
|
|
1296
|
+
var init_validation = __esm({
|
|
1297
|
+
"src/lib/validation.ts"() {
|
|
1298
|
+
"use strict";
|
|
1299
|
+
AflApiTokenSchema = z.object({
|
|
1300
|
+
token: z.string(),
|
|
1301
|
+
disclaimer: z.string().optional()
|
|
1302
|
+
}).passthrough();
|
|
1303
|
+
CompetitionSchema = z.object({
|
|
1304
|
+
id: z.number(),
|
|
1305
|
+
name: z.string(),
|
|
1306
|
+
code: z.string().optional()
|
|
1307
|
+
}).passthrough();
|
|
1308
|
+
CompetitionListSchema = z.object({
|
|
1309
|
+
competitions: z.array(CompetitionSchema)
|
|
1310
|
+
}).passthrough();
|
|
1311
|
+
CompseasonSchema = z.object({
|
|
1312
|
+
id: z.number(),
|
|
1313
|
+
name: z.string(),
|
|
1314
|
+
shortName: z.string().optional(),
|
|
1315
|
+
currentRoundNumber: z.number().optional()
|
|
1316
|
+
}).passthrough();
|
|
1317
|
+
CompseasonListSchema = z.object({
|
|
1318
|
+
compSeasons: z.array(CompseasonSchema)
|
|
1319
|
+
}).passthrough();
|
|
1320
|
+
RoundSchema = z.object({
|
|
1321
|
+
id: z.number(),
|
|
1322
|
+
/** Provider ID used by /cfs/ endpoints (e.g. "CD_R202501401"). */
|
|
1323
|
+
providerId: z.string().optional(),
|
|
1324
|
+
name: z.string(),
|
|
1325
|
+
abbreviation: z.string().optional(),
|
|
1326
|
+
roundNumber: z.number(),
|
|
1327
|
+
utcStartTime: z.string().optional(),
|
|
1328
|
+
utcEndTime: z.string().optional()
|
|
1329
|
+
}).passthrough();
|
|
1330
|
+
RoundListSchema = z.object({
|
|
1331
|
+
rounds: z.array(RoundSchema)
|
|
1332
|
+
}).passthrough();
|
|
1333
|
+
ScoreSchema = z.object({
|
|
1334
|
+
totalScore: z.number(),
|
|
1335
|
+
goals: z.number(),
|
|
1336
|
+
behinds: z.number(),
|
|
1337
|
+
superGoals: z.number().nullable().optional()
|
|
1338
|
+
}).passthrough();
|
|
1339
|
+
PeriodScoreSchema = z.object({
|
|
1340
|
+
periodNumber: z.number(),
|
|
1341
|
+
score: ScoreSchema
|
|
1342
|
+
}).passthrough();
|
|
1343
|
+
TeamScoreSchema = z.object({
|
|
1344
|
+
matchScore: ScoreSchema,
|
|
1345
|
+
periodScore: z.array(PeriodScoreSchema).optional(),
|
|
1346
|
+
rushedBehinds: z.number().optional(),
|
|
1347
|
+
minutesInFront: z.number().optional()
|
|
1348
|
+
}).passthrough();
|
|
1349
|
+
CfsMatchTeamSchema = z.object({
|
|
1350
|
+
name: z.string(),
|
|
1351
|
+
teamId: z.string(),
|
|
1352
|
+
abbr: z.string().optional(),
|
|
1353
|
+
nickname: z.string().optional()
|
|
1354
|
+
}).passthrough();
|
|
1355
|
+
CfsMatchSchema = z.object({
|
|
1356
|
+
matchId: z.string(),
|
|
1357
|
+
name: z.string().optional(),
|
|
1358
|
+
status: z.string(),
|
|
1359
|
+
utcStartTime: z.string(),
|
|
1360
|
+
homeTeamId: z.string(),
|
|
1361
|
+
awayTeamId: z.string(),
|
|
1362
|
+
homeTeam: CfsMatchTeamSchema,
|
|
1363
|
+
awayTeam: CfsMatchTeamSchema,
|
|
1364
|
+
round: z.string().optional(),
|
|
1365
|
+
abbr: z.string().optional()
|
|
1366
|
+
}).passthrough();
|
|
1367
|
+
CfsScoreSchema = z.object({
|
|
1368
|
+
status: z.string(),
|
|
1369
|
+
matchId: z.string(),
|
|
1370
|
+
homeTeamScore: TeamScoreSchema,
|
|
1371
|
+
awayTeamScore: TeamScoreSchema
|
|
1372
|
+
}).passthrough();
|
|
1373
|
+
CfsVenueSchema = z.object({
|
|
1374
|
+
name: z.string(),
|
|
1375
|
+
venueId: z.string().optional(),
|
|
1376
|
+
state: z.string().optional(),
|
|
1377
|
+
timeZone: z.string().optional()
|
|
1378
|
+
}).passthrough();
|
|
1379
|
+
MatchItemSchema = z.object({
|
|
1380
|
+
match: CfsMatchSchema,
|
|
1381
|
+
score: CfsScoreSchema.nullish(),
|
|
1382
|
+
venue: CfsVenueSchema.optional(),
|
|
1383
|
+
round: z.object({
|
|
1384
|
+
name: z.string(),
|
|
1385
|
+
roundId: z.string(),
|
|
1386
|
+
roundNumber: z.number()
|
|
1387
|
+
}).passthrough().optional()
|
|
1388
|
+
}).passthrough();
|
|
1389
|
+
MatchItemListSchema = z.object({
|
|
1390
|
+
roundId: z.string().optional(),
|
|
1391
|
+
items: z.array(MatchItemSchema)
|
|
1392
|
+
}).passthrough();
|
|
1393
|
+
CfsPlayerInnerSchema = z.object({
|
|
1394
|
+
playerId: z.string(),
|
|
1395
|
+
playerName: z.object({
|
|
1396
|
+
givenName: z.string(),
|
|
1397
|
+
surname: z.string()
|
|
1398
|
+
}).passthrough(),
|
|
1399
|
+
captain: z.boolean().optional(),
|
|
1400
|
+
playerJumperNumber: z.number().optional()
|
|
1401
|
+
}).passthrough();
|
|
1402
|
+
PlayerGameStatsSchema = z.object({
|
|
1403
|
+
goals: z.number().optional(),
|
|
1404
|
+
behinds: z.number().optional(),
|
|
1405
|
+
kicks: z.number().optional(),
|
|
1406
|
+
handballs: z.number().optional(),
|
|
1407
|
+
disposals: z.number().optional(),
|
|
1408
|
+
marks: z.number().optional(),
|
|
1409
|
+
bounces: z.number().optional(),
|
|
1410
|
+
tackles: z.number().optional(),
|
|
1411
|
+
contestedPossessions: z.number().optional(),
|
|
1412
|
+
uncontestedPossessions: z.number().optional(),
|
|
1413
|
+
totalPossessions: z.number().optional(),
|
|
1414
|
+
inside50s: z.number().optional(),
|
|
1415
|
+
marksInside50: z.number().optional(),
|
|
1416
|
+
contestedMarks: z.number().optional(),
|
|
1417
|
+
hitouts: z.number().optional(),
|
|
1418
|
+
onePercenters: z.number().optional(),
|
|
1419
|
+
disposalEfficiency: z.number().optional(),
|
|
1420
|
+
clangers: z.number().optional(),
|
|
1421
|
+
freesFor: z.number().optional(),
|
|
1422
|
+
freesAgainst: z.number().optional(),
|
|
1423
|
+
dreamTeamPoints: z.number().optional(),
|
|
1424
|
+
clearances: z.object({
|
|
1425
|
+
centreClearances: z.number().optional(),
|
|
1426
|
+
stoppageClearances: z.number().optional(),
|
|
1427
|
+
totalClearances: z.number().optional()
|
|
1428
|
+
}).passthrough().optional(),
|
|
1429
|
+
rebound50s: z.number().optional(),
|
|
1430
|
+
goalAssists: z.number().optional(),
|
|
1431
|
+
goalAccuracy: z.number().optional(),
|
|
1432
|
+
turnovers: z.number().optional(),
|
|
1433
|
+
intercepts: z.number().optional(),
|
|
1434
|
+
tacklesInside50: z.number().optional(),
|
|
1435
|
+
shotsAtGoal: z.number().optional(),
|
|
1436
|
+
metresGained: z.number().optional(),
|
|
1437
|
+
scoreInvolvements: z.number().optional(),
|
|
1438
|
+
ratingPoints: z.number().optional(),
|
|
1439
|
+
extendedStats: z.object({
|
|
1440
|
+
effectiveDisposals: z.number().optional(),
|
|
1441
|
+
effectiveKicks: z.number().optional(),
|
|
1442
|
+
kickEfficiency: z.number().optional(),
|
|
1443
|
+
kickToHandballRatio: z.number().optional(),
|
|
1444
|
+
pressureActs: z.number().optional(),
|
|
1445
|
+
defHalfPressureActs: z.number().optional(),
|
|
1446
|
+
spoils: z.number().optional(),
|
|
1447
|
+
hitoutsToAdvantage: z.number().optional(),
|
|
1448
|
+
hitoutWinPercentage: z.number().optional(),
|
|
1449
|
+
hitoutToAdvantageRate: z.number().optional(),
|
|
1450
|
+
groundBallGets: z.number().optional(),
|
|
1451
|
+
f50GroundBallGets: z.number().optional(),
|
|
1452
|
+
interceptMarks: z.number().optional(),
|
|
1453
|
+
marksOnLead: z.number().optional(),
|
|
1454
|
+
contestedPossessionRate: z.number().optional(),
|
|
1455
|
+
contestOffOneOnOnes: z.number().optional(),
|
|
1456
|
+
contestOffWins: z.number().optional(),
|
|
1457
|
+
contestOffWinsPercentage: z.number().optional(),
|
|
1458
|
+
contestDefOneOnOnes: z.number().optional(),
|
|
1459
|
+
contestDefLosses: z.number().optional(),
|
|
1460
|
+
contestDefLossPercentage: z.number().optional(),
|
|
1461
|
+
centreBounceAttendances: z.number().optional(),
|
|
1462
|
+
kickins: z.number().optional(),
|
|
1463
|
+
kickinsPlayon: z.number().optional(),
|
|
1464
|
+
ruckContests: z.number().optional(),
|
|
1465
|
+
scoreLaunches: z.number().optional()
|
|
1466
|
+
}).passthrough().optional()
|
|
1467
|
+
}).passthrough();
|
|
1468
|
+
PlayerStatsItemSchema = z.object({
|
|
1469
|
+
player: z.object({
|
|
1470
|
+
player: z.object({
|
|
1471
|
+
position: z.string().optional(),
|
|
1472
|
+
player: CfsPlayerInnerSchema
|
|
1473
|
+
}).passthrough(),
|
|
1474
|
+
jumperNumber: z.number().optional()
|
|
1475
|
+
}).passthrough(),
|
|
1476
|
+
teamId: z.string(),
|
|
1477
|
+
playerStats: z.object({
|
|
1478
|
+
stats: PlayerGameStatsSchema,
|
|
1479
|
+
timeOnGroundPercentage: z.number().optional()
|
|
1480
|
+
}).passthrough()
|
|
1481
|
+
}).passthrough();
|
|
1482
|
+
PlayerStatsListSchema = z.object({
|
|
1483
|
+
homeTeamPlayerStats: z.array(PlayerStatsItemSchema),
|
|
1484
|
+
awayTeamPlayerStats: z.array(PlayerStatsItemSchema)
|
|
1485
|
+
}).passthrough();
|
|
1486
|
+
RosterPlayerSchema = z.object({
|
|
1487
|
+
player: z.object({
|
|
1488
|
+
position: z.string().optional(),
|
|
1489
|
+
player: CfsPlayerInnerSchema
|
|
1490
|
+
}).passthrough(),
|
|
1491
|
+
jumperNumber: z.number().optional()
|
|
1492
|
+
}).passthrough();
|
|
1493
|
+
TeamPlayersSchema = z.object({
|
|
1494
|
+
teamId: z.string(),
|
|
1495
|
+
players: z.array(RosterPlayerSchema)
|
|
1496
|
+
}).passthrough();
|
|
1497
|
+
MatchRosterSchema = z.object({
|
|
1498
|
+
match: CfsMatchSchema,
|
|
1499
|
+
teamPlayers: z.array(TeamPlayersSchema)
|
|
1500
|
+
}).passthrough();
|
|
1501
|
+
TeamItemSchema = z.object({
|
|
1502
|
+
id: z.number(),
|
|
1503
|
+
name: z.string(),
|
|
1504
|
+
abbreviation: z.string().optional(),
|
|
1505
|
+
teamType: z.string().optional()
|
|
1506
|
+
}).passthrough();
|
|
1507
|
+
TeamListSchema = z.object({
|
|
1508
|
+
teams: z.array(TeamItemSchema)
|
|
1509
|
+
}).passthrough();
|
|
1510
|
+
SquadPlayerInnerSchema = z.object({
|
|
1511
|
+
id: z.number(),
|
|
1512
|
+
providerId: z.string().optional(),
|
|
1513
|
+
firstName: z.string(),
|
|
1514
|
+
surname: z.string(),
|
|
1515
|
+
dateOfBirth: z.string().optional(),
|
|
1516
|
+
heightInCm: z.number().optional(),
|
|
1517
|
+
weightInKg: z.number().optional(),
|
|
1518
|
+
draftYear: z.string().optional(),
|
|
1519
|
+
draftPosition: z.string().optional(),
|
|
1520
|
+
draftType: z.string().optional(),
|
|
1521
|
+
debutYear: z.string().optional(),
|
|
1522
|
+
recruitedFrom: z.string().optional()
|
|
1523
|
+
}).passthrough();
|
|
1524
|
+
SquadPlayerItemSchema = z.object({
|
|
1525
|
+
player: SquadPlayerInnerSchema,
|
|
1526
|
+
jumperNumber: z.number().optional(),
|
|
1527
|
+
position: z.string().optional()
|
|
1528
|
+
}).passthrough();
|
|
1529
|
+
SquadSchema = z.object({
|
|
1530
|
+
team: z.object({
|
|
1531
|
+
name: z.string()
|
|
1532
|
+
}).passthrough().optional(),
|
|
1533
|
+
players: z.array(SquadPlayerItemSchema)
|
|
1534
|
+
}).passthrough();
|
|
1535
|
+
SquadListSchema = z.object({
|
|
1536
|
+
squad: SquadSchema
|
|
1537
|
+
}).passthrough();
|
|
1538
|
+
WinLossRecordSchema = z.object({
|
|
1539
|
+
wins: z.number(),
|
|
1540
|
+
losses: z.number(),
|
|
1541
|
+
draws: z.number(),
|
|
1542
|
+
played: z.number().optional()
|
|
1543
|
+
}).passthrough();
|
|
1544
|
+
LadderEntryRawSchema = z.object({
|
|
1545
|
+
position: z.number(),
|
|
1546
|
+
team: z.object({
|
|
1547
|
+
name: z.string(),
|
|
1548
|
+
id: z.number().optional(),
|
|
1549
|
+
abbreviation: z.string().optional()
|
|
1550
|
+
}).passthrough(),
|
|
1551
|
+
played: z.number().optional(),
|
|
1552
|
+
pointsFor: z.number().optional(),
|
|
1553
|
+
pointsAgainst: z.number().optional(),
|
|
1554
|
+
thisSeasonRecord: z.object({
|
|
1555
|
+
aggregatePoints: z.number().optional(),
|
|
1556
|
+
percentage: z.number().optional(),
|
|
1557
|
+
winLossRecord: WinLossRecordSchema.optional()
|
|
1558
|
+
}).passthrough().optional(),
|
|
1559
|
+
form: z.string().optional()
|
|
1560
|
+
}).passthrough();
|
|
1561
|
+
LadderResponseSchema = z.object({
|
|
1562
|
+
ladders: z.array(
|
|
1563
|
+
z.object({
|
|
1564
|
+
entries: z.array(LadderEntryRawSchema)
|
|
1565
|
+
}).passthrough()
|
|
1566
|
+
),
|
|
1567
|
+
round: z.object({
|
|
1568
|
+
roundNumber: z.number(),
|
|
1569
|
+
name: z.string().optional()
|
|
1570
|
+
}).passthrough().optional()
|
|
1571
|
+
}).passthrough();
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
// src/sources/afl-api.ts
|
|
1576
|
+
var TOKEN_URL, API_BASE, CFS_BASE, AflApiClient;
|
|
1577
|
+
var init_afl_api = __esm({
|
|
1578
|
+
"src/sources/afl-api.ts"() {
|
|
1579
|
+
"use strict";
|
|
1580
|
+
init_errors();
|
|
1581
|
+
init_result();
|
|
1582
|
+
init_validation();
|
|
1583
|
+
TOKEN_URL = "https://api.afl.com.au/cfs/afl/WMCTok";
|
|
1584
|
+
API_BASE = "https://aflapi.afl.com.au/afl/v2";
|
|
1585
|
+
CFS_BASE = "https://api.afl.com.au/cfs/afl";
|
|
1586
|
+
AflApiClient = class {
|
|
1587
|
+
fetchFn;
|
|
1588
|
+
tokenUrl;
|
|
1589
|
+
cachedToken = null;
|
|
1590
|
+
constructor(options) {
|
|
1591
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
1592
|
+
this.tokenUrl = options?.tokenUrl ?? TOKEN_URL;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Authenticate with the WMCTok token endpoint and cache the token.
|
|
1596
|
+
*
|
|
1597
|
+
* @returns The access token on success, or an error Result.
|
|
1598
|
+
*/
|
|
1599
|
+
async authenticate() {
|
|
1600
|
+
try {
|
|
1601
|
+
const response = await this.fetchFn(this.tokenUrl, {
|
|
1602
|
+
method: "POST",
|
|
1603
|
+
headers: { "Content-Length": "0" }
|
|
1604
|
+
});
|
|
1605
|
+
if (!response.ok) {
|
|
1606
|
+
return err(new AflApiError(`Token request failed: ${response.status}`, response.status));
|
|
1607
|
+
}
|
|
1608
|
+
const json = await response.json();
|
|
1609
|
+
const parsed = AflApiTokenSchema.safeParse(json);
|
|
1610
|
+
if (!parsed.success) {
|
|
1611
|
+
return err(new AflApiError("Invalid token response format"));
|
|
1612
|
+
}
|
|
1613
|
+
const ttlMs = 30 * 60 * 1e3;
|
|
1614
|
+
this.cachedToken = {
|
|
1615
|
+
accessToken: parsed.data.token,
|
|
1616
|
+
expiresAt: Date.now() + ttlMs
|
|
1617
|
+
};
|
|
1618
|
+
return ok(parsed.data.token);
|
|
1619
|
+
} catch (cause) {
|
|
1620
|
+
return err(
|
|
1621
|
+
new AflApiError(
|
|
1622
|
+
`Token request failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1623
|
+
)
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Whether the cached token is still valid (not expired).
|
|
1629
|
+
*/
|
|
1630
|
+
get isAuthenticated() {
|
|
1631
|
+
return this.cachedToken !== null && Date.now() < this.cachedToken.expiresAt;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Perform an authenticated fetch, automatically adding the bearer token.
|
|
1635
|
+
* Retries once on 401 by re-authenticating.
|
|
1636
|
+
*
|
|
1637
|
+
* @param url - The URL to fetch.
|
|
1638
|
+
* @param init - Additional fetch options.
|
|
1639
|
+
* @returns The Response on success, or an error Result.
|
|
1640
|
+
*/
|
|
1641
|
+
async authedFetch(url, init) {
|
|
1642
|
+
if (!this.isAuthenticated) {
|
|
1643
|
+
const authResult = await this.authenticate();
|
|
1644
|
+
if (!authResult.success) {
|
|
1645
|
+
return authResult;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const doFetch = async () => {
|
|
1649
|
+
const token = this.cachedToken;
|
|
1650
|
+
if (!token) {
|
|
1651
|
+
throw new AflApiError("No cached token available");
|
|
1652
|
+
}
|
|
1653
|
+
const headers = new Headers(init?.headers);
|
|
1654
|
+
headers.set("x-media-mis-token", token.accessToken);
|
|
1655
|
+
return this.fetchFn(url, { ...init, headers });
|
|
1656
|
+
};
|
|
1657
|
+
try {
|
|
1658
|
+
let response = await doFetch();
|
|
1659
|
+
if (response.status === 401) {
|
|
1660
|
+
const authResult = await this.authenticate();
|
|
1661
|
+
if (!authResult.success) {
|
|
1662
|
+
return authResult;
|
|
1663
|
+
}
|
|
1664
|
+
response = await doFetch();
|
|
1665
|
+
}
|
|
1666
|
+
if (!response.ok) {
|
|
1667
|
+
return err(
|
|
1668
|
+
new AflApiError(
|
|
1669
|
+
`Request failed: ${response.status} ${response.statusText}`,
|
|
1670
|
+
response.status
|
|
1671
|
+
)
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
return ok(response);
|
|
1675
|
+
} catch (cause) {
|
|
1676
|
+
return err(
|
|
1677
|
+
new AflApiError(
|
|
1678
|
+
`Request failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1679
|
+
)
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Fetch JSON from a URL, validate with a Zod schema, and return a typed Result.
|
|
1685
|
+
*
|
|
1686
|
+
* @param url - The URL to fetch.
|
|
1687
|
+
* @param schema - Zod schema to validate the response against.
|
|
1688
|
+
* @returns Validated data on success, or an error Result.
|
|
1689
|
+
*/
|
|
1690
|
+
async fetchJson(url, schema) {
|
|
1691
|
+
const isPublic = url.startsWith(API_BASE);
|
|
1692
|
+
let response;
|
|
1693
|
+
if (isPublic) {
|
|
1694
|
+
try {
|
|
1695
|
+
response = await this.fetchFn(url);
|
|
1696
|
+
if (!response.ok) {
|
|
1697
|
+
return err(
|
|
1698
|
+
new AflApiError(
|
|
1699
|
+
`Request failed: ${response.status} ${response.statusText}`,
|
|
1700
|
+
response.status
|
|
1701
|
+
)
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
} catch (cause) {
|
|
1705
|
+
return err(
|
|
1706
|
+
new AflApiError(
|
|
1707
|
+
`Request failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1708
|
+
)
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
} else {
|
|
1712
|
+
const fetchResult = await this.authedFetch(url);
|
|
1713
|
+
if (!fetchResult.success) {
|
|
1714
|
+
return fetchResult;
|
|
1715
|
+
}
|
|
1716
|
+
response = fetchResult.data;
|
|
1717
|
+
}
|
|
1718
|
+
try {
|
|
1719
|
+
const json = await response.json();
|
|
1720
|
+
const parsed = schema.safeParse(json);
|
|
1721
|
+
if (!parsed.success) {
|
|
1722
|
+
return err(
|
|
1723
|
+
new ValidationError("Response validation failed", [
|
|
1724
|
+
{ path: url, message: String(parsed.error) }
|
|
1725
|
+
])
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
return ok(parsed.data);
|
|
1729
|
+
} catch (cause) {
|
|
1730
|
+
return err(
|
|
1731
|
+
new AflApiError(
|
|
1732
|
+
`JSON parse failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
1733
|
+
)
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Resolve a competition code (e.g. "AFLM") to its API competition ID.
|
|
1739
|
+
*
|
|
1740
|
+
* @param code - The competition code to resolve.
|
|
1741
|
+
* @returns The competition ID string on success.
|
|
1742
|
+
*/
|
|
1743
|
+
async resolveCompetitionId(code) {
|
|
1744
|
+
const result = await this.fetchJson(
|
|
1745
|
+
`${API_BASE}/competitions?pageSize=50`,
|
|
1746
|
+
CompetitionListSchema
|
|
1747
|
+
);
|
|
1748
|
+
if (!result.success) {
|
|
1749
|
+
return result;
|
|
1750
|
+
}
|
|
1751
|
+
const apiCode = code === "AFLM" ? "AFL" : code;
|
|
1752
|
+
const competition = result.data.competitions.find((c) => c.code === apiCode);
|
|
1753
|
+
if (!competition) {
|
|
1754
|
+
return err(new AflApiError(`Competition not found for code: ${code}`));
|
|
1755
|
+
}
|
|
1756
|
+
return ok(competition.id);
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Resolve a season (compseason) ID from a competition ID and year.
|
|
1760
|
+
*
|
|
1761
|
+
* @param competitionId - The competition ID (from {@link resolveCompetitionId}).
|
|
1762
|
+
* @param year - The season year (e.g. 2024).
|
|
1763
|
+
* @returns The compseason ID string on success.
|
|
1764
|
+
*/
|
|
1765
|
+
async resolveSeasonId(competitionId, year) {
|
|
1766
|
+
const result = await this.fetchJson(
|
|
1767
|
+
`${API_BASE}/competitions/${competitionId}/compseasons?pageSize=100`,
|
|
1768
|
+
CompseasonListSchema
|
|
1769
|
+
);
|
|
1770
|
+
if (!result.success) {
|
|
1771
|
+
return result;
|
|
1772
|
+
}
|
|
1773
|
+
const yearStr = String(year);
|
|
1774
|
+
const season = result.data.compSeasons.find((cs) => cs.name.includes(yearStr));
|
|
1775
|
+
if (!season) {
|
|
1776
|
+
return err(new AflApiError(`Season not found for year: ${year}`));
|
|
1777
|
+
}
|
|
1778
|
+
return ok(season.id);
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Resolve a season ID from a competition code and year in one step.
|
|
1782
|
+
*
|
|
1783
|
+
* @param code - The competition code (e.g. "AFLM").
|
|
1784
|
+
* @param year - The season year (e.g. 2025).
|
|
1785
|
+
* @returns The compseason ID on success.
|
|
1786
|
+
*/
|
|
1787
|
+
async resolveCompSeason(code, year) {
|
|
1788
|
+
const compResult = await this.resolveCompetitionId(code);
|
|
1789
|
+
if (!compResult.success) return compResult;
|
|
1790
|
+
return this.resolveSeasonId(compResult.data, year);
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Fetch all rounds for a season with their metadata.
|
|
1794
|
+
*
|
|
1795
|
+
* @param seasonId - The compseason ID (from {@link resolveSeasonId}).
|
|
1796
|
+
* @returns Array of round objects on success.
|
|
1797
|
+
*/
|
|
1798
|
+
async resolveRounds(seasonId) {
|
|
1799
|
+
const result = await this.fetchJson(
|
|
1800
|
+
`${API_BASE}/compseasons/${seasonId}/rounds?pageSize=50`,
|
|
1801
|
+
RoundListSchema
|
|
1802
|
+
);
|
|
1803
|
+
if (!result.success) {
|
|
1804
|
+
return result;
|
|
1805
|
+
}
|
|
1806
|
+
return ok(result.data.rounds);
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Fetch match items for a round using the /cfs/ endpoint.
|
|
1810
|
+
*
|
|
1811
|
+
* @param roundProviderId - The round provider ID (e.g. "CD_R202501401").
|
|
1812
|
+
* @returns Array of match items on success.
|
|
1813
|
+
*/
|
|
1814
|
+
async fetchRoundMatchItems(roundProviderId) {
|
|
1815
|
+
const result = await this.fetchJson(
|
|
1816
|
+
`${CFS_BASE}/matchItems/round/${roundProviderId}`,
|
|
1817
|
+
MatchItemListSchema
|
|
1818
|
+
);
|
|
1819
|
+
if (!result.success) {
|
|
1820
|
+
return result;
|
|
1821
|
+
}
|
|
1822
|
+
return ok(result.data.items);
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Fetch match items for a round by resolving the round provider ID from season and round number.
|
|
1826
|
+
*
|
|
1827
|
+
* @param seasonId - The compseason ID.
|
|
1828
|
+
* @param roundNumber - The round number.
|
|
1829
|
+
* @returns Array of match items on success.
|
|
1830
|
+
*/
|
|
1831
|
+
async fetchRoundMatchItemsByNumber(seasonId, roundNumber) {
|
|
1832
|
+
const roundsResult = await this.resolveRounds(seasonId);
|
|
1833
|
+
if (!roundsResult.success) {
|
|
1834
|
+
return roundsResult;
|
|
1835
|
+
}
|
|
1836
|
+
const round = roundsResult.data.find((r) => r.roundNumber === roundNumber);
|
|
1837
|
+
if (!round?.providerId) {
|
|
1838
|
+
return err(new AflApiError(`Round not found or missing providerId: round ${roundNumber}`));
|
|
1839
|
+
}
|
|
1840
|
+
return this.fetchRoundMatchItems(round.providerId);
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Fetch match items for all completed rounds in a season.
|
|
1844
|
+
*
|
|
1845
|
+
* @param seasonId - The compseason ID.
|
|
1846
|
+
* @returns Aggregated array of match items from all completed rounds.
|
|
1847
|
+
*/
|
|
1848
|
+
async fetchSeasonMatchItems(seasonId) {
|
|
1849
|
+
const roundsResult = await this.resolveRounds(seasonId);
|
|
1850
|
+
if (!roundsResult.success) {
|
|
1851
|
+
return roundsResult;
|
|
1852
|
+
}
|
|
1853
|
+
const providerIds = roundsResult.data.flatMap((r) => r.providerId ? [r.providerId] : []);
|
|
1854
|
+
const results = await Promise.all(providerIds.map((id) => this.fetchRoundMatchItems(id)));
|
|
1855
|
+
const allItems = [];
|
|
1856
|
+
for (const result of results) {
|
|
1857
|
+
if (!result.success) {
|
|
1858
|
+
return result;
|
|
1859
|
+
}
|
|
1860
|
+
const concluded = result.data.filter(
|
|
1861
|
+
(item) => item.match.status === "CONCLUDED" || item.match.status === "COMPLETE"
|
|
1862
|
+
);
|
|
1863
|
+
allItems.push(...concluded);
|
|
1864
|
+
}
|
|
1865
|
+
return ok(allItems);
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Fetch per-player statistics for a match.
|
|
1869
|
+
*
|
|
1870
|
+
* @param matchProviderId - The match provider ID (e.g. "CD_M20250140101").
|
|
1871
|
+
* @returns Player stats list with home and away arrays.
|
|
1872
|
+
*/
|
|
1873
|
+
async fetchPlayerStats(matchProviderId) {
|
|
1874
|
+
return this.fetchJson(
|
|
1875
|
+
`${CFS_BASE}/playerStats/match/${matchProviderId}`,
|
|
1876
|
+
PlayerStatsListSchema
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Fetch match roster (lineup) for a match.
|
|
1881
|
+
*
|
|
1882
|
+
* @param matchProviderId - The match provider ID (e.g. "CD_M20250140101").
|
|
1883
|
+
* @returns Match roster with team players.
|
|
1884
|
+
*/
|
|
1885
|
+
async fetchMatchRoster(matchProviderId) {
|
|
1886
|
+
return this.fetchJson(`${CFS_BASE}/matchRoster/full/${matchProviderId}`, MatchRosterSchema);
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Fetch team list, optionally filtered by team type.
|
|
1890
|
+
*
|
|
1891
|
+
* @param teamType - Optional filter (e.g. "MEN", "WOMEN").
|
|
1892
|
+
* @returns Array of team items.
|
|
1893
|
+
*/
|
|
1894
|
+
async fetchTeams(teamType) {
|
|
1895
|
+
const result = await this.fetchJson(`${API_BASE}/teams?pageSize=100`, TeamListSchema);
|
|
1896
|
+
if (!result.success) {
|
|
1897
|
+
return result;
|
|
1898
|
+
}
|
|
1899
|
+
if (teamType) {
|
|
1900
|
+
return ok(result.data.teams.filter((t) => t.teamType === teamType));
|
|
1901
|
+
}
|
|
1902
|
+
return ok(result.data.teams);
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Fetch squad (roster) for a team in a specific season.
|
|
1906
|
+
*
|
|
1907
|
+
* @param teamId - The numeric team ID.
|
|
1908
|
+
* @param compSeasonId - The compseason ID.
|
|
1909
|
+
* @returns Squad list response.
|
|
1910
|
+
*/
|
|
1911
|
+
async fetchSquad(teamId, compSeasonId) {
|
|
1912
|
+
return this.fetchJson(
|
|
1913
|
+
`${API_BASE}/squads?teamId=${teamId}&compSeasonId=${compSeasonId}`,
|
|
1914
|
+
SquadListSchema
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Fetch ladder standings for a season (optionally for a specific round).
|
|
1919
|
+
*
|
|
1920
|
+
* @param seasonId - The compseason ID.
|
|
1921
|
+
* @param roundId - Optional round ID (numeric `id`, not `providerId`).
|
|
1922
|
+
* @returns Ladder response with entries.
|
|
1923
|
+
*/
|
|
1924
|
+
async fetchLadder(seasonId, roundId) {
|
|
1925
|
+
let url = `${API_BASE}/compseasons/${seasonId}/ladders`;
|
|
1926
|
+
if (roundId != null) {
|
|
1927
|
+
url += `?roundId=${roundId}`;
|
|
1928
|
+
}
|
|
1929
|
+
return this.fetchJson(url, LadderResponseSchema);
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
// src/lib/squiggle-validation.ts
|
|
1936
|
+
import { z as z2 } from "zod";
|
|
1937
|
+
var SquiggleGameSchema, SquiggleGamesResponseSchema, SquiggleStandingSchema, SquiggleStandingsResponseSchema;
|
|
1938
|
+
var init_squiggle_validation = __esm({
|
|
1939
|
+
"src/lib/squiggle-validation.ts"() {
|
|
1940
|
+
"use strict";
|
|
1941
|
+
SquiggleGameSchema = z2.object({
|
|
1942
|
+
id: z2.number(),
|
|
1943
|
+
year: z2.number(),
|
|
1944
|
+
round: z2.number(),
|
|
1945
|
+
roundname: z2.string(),
|
|
1946
|
+
hteam: z2.string(),
|
|
1947
|
+
ateam: z2.string(),
|
|
1948
|
+
hteamid: z2.number(),
|
|
1949
|
+
ateamid: z2.number(),
|
|
1950
|
+
hscore: z2.number().nullable(),
|
|
1951
|
+
ascore: z2.number().nullable(),
|
|
1952
|
+
hgoals: z2.number().nullable(),
|
|
1953
|
+
agoals: z2.number().nullable(),
|
|
1954
|
+
hbehinds: z2.number().nullable(),
|
|
1955
|
+
abehinds: z2.number().nullable(),
|
|
1956
|
+
winner: z2.string().nullable(),
|
|
1957
|
+
winnerteamid: z2.number().nullable(),
|
|
1958
|
+
venue: z2.string(),
|
|
1959
|
+
date: z2.string(),
|
|
1960
|
+
localtime: z2.string(),
|
|
1961
|
+
tz: z2.string(),
|
|
1962
|
+
unixtime: z2.number(),
|
|
1963
|
+
timestr: z2.string().nullable(),
|
|
1964
|
+
complete: z2.number(),
|
|
1965
|
+
is_final: z2.number(),
|
|
1966
|
+
is_grand_final: z2.number(),
|
|
1967
|
+
updated: z2.string()
|
|
1968
|
+
});
|
|
1969
|
+
SquiggleGamesResponseSchema = z2.object({
|
|
1970
|
+
games: z2.array(SquiggleGameSchema)
|
|
1971
|
+
});
|
|
1972
|
+
SquiggleStandingSchema = z2.object({
|
|
1973
|
+
id: z2.number(),
|
|
1974
|
+
name: z2.string(),
|
|
1975
|
+
rank: z2.number(),
|
|
1976
|
+
played: z2.number(),
|
|
1977
|
+
wins: z2.number(),
|
|
1978
|
+
losses: z2.number(),
|
|
1979
|
+
draws: z2.number(),
|
|
1980
|
+
pts: z2.number(),
|
|
1981
|
+
for: z2.number(),
|
|
1982
|
+
against: z2.number(),
|
|
1983
|
+
percentage: z2.number(),
|
|
1984
|
+
goals_for: z2.number(),
|
|
1985
|
+
goals_against: z2.number(),
|
|
1986
|
+
behinds_for: z2.number(),
|
|
1987
|
+
behinds_against: z2.number()
|
|
1988
|
+
});
|
|
1989
|
+
SquiggleStandingsResponseSchema = z2.object({
|
|
1990
|
+
standings: z2.array(SquiggleStandingSchema)
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
// src/sources/squiggle.ts
|
|
1996
|
+
var SQUIGGLE_BASE, USER_AGENT, SquiggleClient;
|
|
1997
|
+
var init_squiggle = __esm({
|
|
1998
|
+
"src/sources/squiggle.ts"() {
|
|
1999
|
+
"use strict";
|
|
2000
|
+
init_errors();
|
|
2001
|
+
init_result();
|
|
2002
|
+
init_squiggle_validation();
|
|
2003
|
+
SQUIGGLE_BASE = "https://api.squiggle.com.au/";
|
|
2004
|
+
USER_AGENT = "fitzRoy-ts/1.0 (https://github.com/jackemcpherson/fitzRoy-ts)";
|
|
2005
|
+
SquiggleClient = class {
|
|
2006
|
+
fetchFn;
|
|
2007
|
+
constructor(options) {
|
|
2008
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Fetch JSON from the Squiggle API.
|
|
2012
|
+
*/
|
|
2013
|
+
async fetchJson(params) {
|
|
2014
|
+
const url = `${SQUIGGLE_BASE}?${params.toString()}`;
|
|
2015
|
+
try {
|
|
2016
|
+
const response = await this.fetchFn(url, {
|
|
2017
|
+
headers: { "User-Agent": USER_AGENT }
|
|
2018
|
+
});
|
|
2019
|
+
if (!response.ok) {
|
|
2020
|
+
return err(
|
|
2021
|
+
new ScrapeError(`Squiggle request failed: ${response.status} (${url})`, "squiggle")
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
const json = await response.json();
|
|
2025
|
+
return ok(json);
|
|
2026
|
+
} catch (cause) {
|
|
2027
|
+
return err(
|
|
2028
|
+
new ScrapeError(
|
|
2029
|
+
`Squiggle request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
2030
|
+
"squiggle"
|
|
2031
|
+
)
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Fetch games (match results or fixture) from the Squiggle API.
|
|
2037
|
+
*
|
|
2038
|
+
* @param year - Season year.
|
|
2039
|
+
* @param round - Optional round number.
|
|
2040
|
+
* @param complete - Optional completion filter (100 = complete, omit for all).
|
|
2041
|
+
*/
|
|
2042
|
+
async fetchGames(year, round, complete) {
|
|
2043
|
+
const params = new URLSearchParams({ q: "games", year: String(year) });
|
|
2044
|
+
if (round != null) params.set("round", String(round));
|
|
2045
|
+
if (complete != null) params.set("complete", String(complete));
|
|
2046
|
+
const result = await this.fetchJson(params);
|
|
2047
|
+
if (!result.success) return result;
|
|
2048
|
+
const parsed = SquiggleGamesResponseSchema.safeParse(result.data);
|
|
2049
|
+
if (!parsed.success) {
|
|
2050
|
+
return err(
|
|
2051
|
+
new ScrapeError(`Invalid Squiggle games response: ${parsed.error.message}`, "squiggle")
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
return ok(parsed.data);
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Fetch standings (ladder) from the Squiggle API.
|
|
2058
|
+
*
|
|
2059
|
+
* @param year - Season year.
|
|
2060
|
+
* @param round - Optional round number.
|
|
2061
|
+
*/
|
|
2062
|
+
async fetchStandings(year, round) {
|
|
2063
|
+
const params = new URLSearchParams({ q: "standings", year: String(year) });
|
|
2064
|
+
if (round != null) params.set("round", String(round));
|
|
2065
|
+
const result = await this.fetchJson(params);
|
|
2066
|
+
if (!result.success) return result;
|
|
2067
|
+
const parsed = SquiggleStandingsResponseSchema.safeParse(result.data);
|
|
2068
|
+
if (!parsed.success) {
|
|
2069
|
+
return err(
|
|
2070
|
+
new ScrapeError(`Invalid Squiggle standings response: ${parsed.error.message}`, "squiggle")
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
return ok(parsed.data);
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
// src/transforms/squiggle.ts
|
|
2080
|
+
function toMatchStatus2(complete) {
|
|
2081
|
+
if (complete === 100) return "Complete";
|
|
2082
|
+
if (complete > 0) return "Live";
|
|
2083
|
+
return "Upcoming";
|
|
2084
|
+
}
|
|
2085
|
+
function transformSquiggleGamesToResults(games, season) {
|
|
2086
|
+
return games.filter((g) => g.complete === 100).map((g) => ({
|
|
2087
|
+
matchId: `SQ_${g.id}`,
|
|
2088
|
+
season,
|
|
2089
|
+
roundNumber: g.round,
|
|
2090
|
+
roundType: inferRoundType(g.roundname),
|
|
2091
|
+
date: new Date(g.unixtime * 1e3),
|
|
2092
|
+
venue: g.venue,
|
|
2093
|
+
homeTeam: normaliseTeamName(g.hteam),
|
|
2094
|
+
awayTeam: normaliseTeamName(g.ateam),
|
|
2095
|
+
homeGoals: g.hgoals ?? 0,
|
|
2096
|
+
homeBehinds: g.hbehinds ?? 0,
|
|
2097
|
+
homePoints: g.hscore ?? 0,
|
|
2098
|
+
awayGoals: g.agoals ?? 0,
|
|
2099
|
+
awayBehinds: g.abehinds ?? 0,
|
|
2100
|
+
awayPoints: g.ascore ?? 0,
|
|
2101
|
+
margin: (g.hscore ?? 0) - (g.ascore ?? 0),
|
|
2102
|
+
q1Home: null,
|
|
2103
|
+
q2Home: null,
|
|
2104
|
+
q3Home: null,
|
|
2105
|
+
q4Home: null,
|
|
2106
|
+
q1Away: null,
|
|
2107
|
+
q2Away: null,
|
|
2108
|
+
q3Away: null,
|
|
2109
|
+
q4Away: null,
|
|
2110
|
+
status: "Complete",
|
|
2111
|
+
attendance: null,
|
|
2112
|
+
venueState: null,
|
|
2113
|
+
venueTimezone: g.tz || null,
|
|
2114
|
+
homeRushedBehinds: null,
|
|
2115
|
+
awayRushedBehinds: null,
|
|
2116
|
+
homeMinutesInFront: null,
|
|
2117
|
+
awayMinutesInFront: null,
|
|
2118
|
+
source: "squiggle",
|
|
2119
|
+
competition: "AFLM"
|
|
2120
|
+
}));
|
|
2121
|
+
}
|
|
2122
|
+
function transformSquiggleGamesToFixture(games, season) {
|
|
2123
|
+
return games.map((g) => ({
|
|
2124
|
+
matchId: `SQ_${g.id}`,
|
|
2125
|
+
season,
|
|
2126
|
+
roundNumber: g.round,
|
|
2127
|
+
roundType: inferRoundType(g.roundname),
|
|
2128
|
+
date: new Date(g.unixtime * 1e3),
|
|
2129
|
+
venue: g.venue,
|
|
2130
|
+
homeTeam: normaliseTeamName(g.hteam),
|
|
2131
|
+
awayTeam: normaliseTeamName(g.ateam),
|
|
2132
|
+
status: toMatchStatus2(g.complete),
|
|
2133
|
+
competition: "AFLM"
|
|
2134
|
+
}));
|
|
2135
|
+
}
|
|
2136
|
+
function transformSquiggleStandings(standings) {
|
|
2137
|
+
return standings.map((s) => ({
|
|
2138
|
+
position: s.rank,
|
|
2139
|
+
team: normaliseTeamName(s.name),
|
|
2140
|
+
played: s.played,
|
|
2141
|
+
wins: s.wins,
|
|
2142
|
+
losses: s.losses,
|
|
2143
|
+
draws: s.draws,
|
|
2144
|
+
pointsFor: s.for,
|
|
2145
|
+
pointsAgainst: s.against,
|
|
2146
|
+
percentage: s.percentage,
|
|
2147
|
+
premiershipsPoints: s.pts,
|
|
2148
|
+
form: null
|
|
2149
|
+
}));
|
|
2150
|
+
}
|
|
2151
|
+
var init_squiggle2 = __esm({
|
|
2152
|
+
"src/transforms/squiggle.ts"() {
|
|
2153
|
+
"use strict";
|
|
2154
|
+
init_team_mapping();
|
|
2155
|
+
init_match_results();
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
// src/api/fixture.ts
|
|
2160
|
+
function toFixture(item, season, fallbackRoundNumber, competition) {
|
|
2161
|
+
return {
|
|
2162
|
+
matchId: item.match.matchId,
|
|
2163
|
+
season,
|
|
2164
|
+
roundNumber: item.round?.roundNumber ?? fallbackRoundNumber,
|
|
2165
|
+
roundType: inferRoundType(item.round?.name ?? ""),
|
|
2166
|
+
date: new Date(item.match.utcStartTime),
|
|
2167
|
+
venue: item.venue?.name ?? "",
|
|
2168
|
+
homeTeam: normaliseTeamName(item.match.homeTeam.name),
|
|
2169
|
+
awayTeam: normaliseTeamName(item.match.awayTeam.name),
|
|
2170
|
+
status: toMatchStatus(item.match.status),
|
|
2171
|
+
competition
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
async function fetchFixture(query) {
|
|
2175
|
+
const competition = query.competition ?? "AFLM";
|
|
2176
|
+
if (query.source === "squiggle") {
|
|
2177
|
+
const client2 = new SquiggleClient();
|
|
2178
|
+
const result = await client2.fetchGames(query.season, query.round ?? void 0);
|
|
2179
|
+
if (!result.success) return result;
|
|
2180
|
+
return ok(transformSquiggleGamesToFixture(result.data.games, query.season));
|
|
2181
|
+
}
|
|
2182
|
+
if (query.source === "footywire") {
|
|
2183
|
+
const fwClient = new FootyWireClient();
|
|
2184
|
+
const result = await fwClient.fetchSeasonFixture(query.season);
|
|
2185
|
+
if (!result.success) return result;
|
|
2186
|
+
if (query.round != null) {
|
|
2187
|
+
return ok(result.data.filter((f) => f.roundNumber === query.round));
|
|
2188
|
+
}
|
|
2189
|
+
return result;
|
|
2190
|
+
}
|
|
2191
|
+
if (query.source !== "afl-api") {
|
|
2192
|
+
return err(
|
|
2193
|
+
new UnsupportedSourceError(
|
|
2194
|
+
"Fixture data is only available from the AFL API, FootyWire, or Squiggle sources.",
|
|
2195
|
+
query.source
|
|
2196
|
+
)
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
const client = new AflApiClient();
|
|
2200
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
2201
|
+
if (!seasonResult.success) return seasonResult;
|
|
2202
|
+
if (query.round != null) {
|
|
2203
|
+
const itemsResult = await client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round);
|
|
2204
|
+
if (!itemsResult.success) return itemsResult;
|
|
2205
|
+
return ok(itemsResult.data.map((item) => toFixture(item, query.season, 0, competition)));
|
|
2206
|
+
}
|
|
2207
|
+
const roundsResult = await client.resolveRounds(seasonResult.data);
|
|
2208
|
+
if (!roundsResult.success) return roundsResult;
|
|
2209
|
+
const roundProviderIds = roundsResult.data.flatMap(
|
|
2210
|
+
(r) => r.providerId ? [{ providerId: r.providerId, roundNumber: r.roundNumber }] : []
|
|
2211
|
+
);
|
|
2212
|
+
const roundResults = await Promise.all(
|
|
2213
|
+
roundProviderIds.map((r) => client.fetchRoundMatchItems(r.providerId))
|
|
2214
|
+
);
|
|
2215
|
+
const fixtures = [];
|
|
2216
|
+
for (let i = 0; i < roundResults.length; i++) {
|
|
2217
|
+
const result = roundResults[i];
|
|
2218
|
+
if (!result?.success) continue;
|
|
2219
|
+
const roundNumber = roundProviderIds[i]?.roundNumber ?? 0;
|
|
2220
|
+
for (const item of result.data) {
|
|
2221
|
+
fixtures.push(toFixture(item, query.season, roundNumber, competition));
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
return ok(fixtures);
|
|
2225
|
+
}
|
|
2226
|
+
var init_fixture = __esm({
|
|
2227
|
+
"src/api/fixture.ts"() {
|
|
2228
|
+
"use strict";
|
|
2229
|
+
init_errors();
|
|
2230
|
+
init_result();
|
|
2231
|
+
init_team_mapping();
|
|
2232
|
+
init_afl_api();
|
|
2233
|
+
init_footywire();
|
|
2234
|
+
init_squiggle();
|
|
2235
|
+
init_match_results();
|
|
2236
|
+
init_squiggle2();
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
// src/transforms/afl-tables-player-stats.ts
|
|
2241
|
+
import * as cheerio4 from "cheerio";
|
|
2242
|
+
function parseName(raw) {
|
|
2243
|
+
const cleaned = raw.replace(/[↑↓]/g, "").trim();
|
|
2244
|
+
const parts = cleaned.split(",").map((s) => s.trim());
|
|
2245
|
+
const surname = parts[0] ?? "";
|
|
2246
|
+
const givenName = parts[1] ?? "";
|
|
2247
|
+
return {
|
|
2248
|
+
givenName,
|
|
2249
|
+
surname,
|
|
2250
|
+
displayName: givenName ? `${givenName} ${surname}` : surname
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
function parseAflTablesGameStats(html, matchId, season, roundNumber) {
|
|
2254
|
+
const $ = cheerio4.load(html);
|
|
2255
|
+
const stats = [];
|
|
2256
|
+
$("table.sortable").each((_tableIdx, table) => {
|
|
2257
|
+
const headerText = $(table).find("thead tr").first().text().trim();
|
|
2258
|
+
const teamMatch = /^(\w[\w\s]+?)\s+Match Statistics/i.exec(headerText);
|
|
2259
|
+
if (!teamMatch) return;
|
|
2260
|
+
const teamName = normaliseTeamName(teamMatch[1]?.trim() ?? "");
|
|
2261
|
+
$(table).find("tbody tr").each((_rowIdx, row) => {
|
|
2262
|
+
const cells = $(row).find("td").map((_, c) => $(c).text().trim()).get();
|
|
2263
|
+
if (cells.length < 24) return;
|
|
2264
|
+
const jumperStr = cells[0] ?? "";
|
|
2265
|
+
const jumperNumber = safeInt(jumperStr.replace(/[↑↓]/g, ""));
|
|
2266
|
+
const { givenName, surname, displayName } = parseName(cells[1] ?? "");
|
|
2267
|
+
stats.push({
|
|
2268
|
+
matchId: `AT_${matchId}`,
|
|
2269
|
+
season,
|
|
2270
|
+
roundNumber,
|
|
2271
|
+
team: teamName,
|
|
2272
|
+
competition: "AFLM",
|
|
2273
|
+
playerId: `AT_${displayName.replace(/\s+/g, "_")}`,
|
|
2274
|
+
givenName,
|
|
2275
|
+
surname,
|
|
2276
|
+
displayName,
|
|
2277
|
+
jumperNumber,
|
|
2278
|
+
kicks: safeInt(cells[2] ?? ""),
|
|
2279
|
+
handballs: safeInt(cells[4] ?? ""),
|
|
2280
|
+
disposals: safeInt(cells[5] ?? ""),
|
|
2281
|
+
marks: safeInt(cells[3] ?? ""),
|
|
2282
|
+
goals: safeInt(cells[6] ?? ""),
|
|
2283
|
+
behinds: safeInt(cells[7] ?? ""),
|
|
2284
|
+
tackles: safeInt(cells[9] ?? ""),
|
|
2285
|
+
hitouts: safeInt(cells[8] ?? ""),
|
|
2286
|
+
freesFor: safeInt(cells[14] ?? ""),
|
|
2287
|
+
freesAgainst: safeInt(cells[15] ?? ""),
|
|
2288
|
+
contestedPossessions: safeInt(cells[17] ?? ""),
|
|
2289
|
+
uncontestedPossessions: safeInt(cells[18] ?? ""),
|
|
2290
|
+
contestedMarks: safeInt(cells[19] ?? ""),
|
|
2291
|
+
intercepts: null,
|
|
2292
|
+
centreClearances: null,
|
|
2293
|
+
stoppageClearances: null,
|
|
2294
|
+
totalClearances: safeInt(cells[12] ?? ""),
|
|
2295
|
+
inside50s: safeInt(cells[11] ?? ""),
|
|
2296
|
+
rebound50s: safeInt(cells[10] ?? ""),
|
|
2297
|
+
clangers: safeInt(cells[13] ?? ""),
|
|
2298
|
+
turnovers: null,
|
|
2299
|
+
onePercenters: safeInt(cells[21] ?? ""),
|
|
2300
|
+
bounces: safeInt(cells[22] ?? ""),
|
|
2301
|
+
goalAssists: safeInt(cells[23] ?? ""),
|
|
2302
|
+
disposalEfficiency: null,
|
|
2303
|
+
metresGained: null,
|
|
2304
|
+
goalAccuracy: null,
|
|
2305
|
+
marksInside50: safeInt(cells[20] ?? ""),
|
|
2306
|
+
tacklesInside50: null,
|
|
2307
|
+
shotsAtGoal: null,
|
|
2308
|
+
scoreInvolvements: null,
|
|
2309
|
+
totalPossessions: null,
|
|
2310
|
+
timeOnGroundPercentage: safeInt(cells[24] ?? ""),
|
|
2311
|
+
ratingPoints: null,
|
|
2312
|
+
dreamTeamPoints: null,
|
|
2313
|
+
effectiveDisposals: null,
|
|
2314
|
+
effectiveKicks: null,
|
|
2315
|
+
kickEfficiency: null,
|
|
2316
|
+
kickToHandballRatio: null,
|
|
2317
|
+
pressureActs: null,
|
|
2318
|
+
defHalfPressureActs: null,
|
|
2319
|
+
spoils: null,
|
|
2320
|
+
hitoutsToAdvantage: null,
|
|
2321
|
+
hitoutWinPercentage: null,
|
|
2322
|
+
hitoutToAdvantageRate: null,
|
|
2323
|
+
groundBallGets: null,
|
|
2324
|
+
f50GroundBallGets: null,
|
|
2325
|
+
interceptMarks: null,
|
|
2326
|
+
marksOnLead: null,
|
|
2327
|
+
contestedPossessionRate: null,
|
|
2328
|
+
contestOffOneOnOnes: null,
|
|
2329
|
+
contestOffWins: null,
|
|
2330
|
+
contestOffWinsPercentage: null,
|
|
2331
|
+
contestDefOneOnOnes: null,
|
|
2332
|
+
contestDefLosses: null,
|
|
2333
|
+
contestDefLossPercentage: null,
|
|
2334
|
+
centreBounceAttendances: null,
|
|
2335
|
+
kickins: null,
|
|
2336
|
+
kickinsPlayon: null,
|
|
2337
|
+
ruckContests: null,
|
|
2338
|
+
scoreLaunches: null,
|
|
2339
|
+
source: "afl-tables"
|
|
2340
|
+
});
|
|
2341
|
+
});
|
|
2342
|
+
});
|
|
2343
|
+
return stats;
|
|
2344
|
+
}
|
|
2345
|
+
function extractGameUrls(seasonHtml) {
|
|
2346
|
+
const $ = cheerio4.load(seasonHtml);
|
|
2347
|
+
const urls = [];
|
|
2348
|
+
$("tr:nth-child(2) td:nth-child(4) a").each((_i, el) => {
|
|
2349
|
+
const href = $(el).attr("href");
|
|
2350
|
+
if (href) {
|
|
2351
|
+
urls.push(href.replace("..", "https://afltables.com/afl"));
|
|
2352
|
+
}
|
|
2353
|
+
});
|
|
2354
|
+
return urls;
|
|
2355
|
+
}
|
|
2356
|
+
var init_afl_tables_player_stats = __esm({
|
|
2357
|
+
"src/transforms/afl-tables-player-stats.ts"() {
|
|
2358
|
+
"use strict";
|
|
2359
|
+
init_parse_utils();
|
|
2360
|
+
init_team_mapping();
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
// src/sources/afl-tables.ts
|
|
2365
|
+
import * as cheerio5 from "cheerio";
|
|
2366
|
+
function parseSeasonPage(html, year) {
|
|
2367
|
+
const $ = cheerio5.load(html);
|
|
2368
|
+
const results = [];
|
|
2369
|
+
let currentRound = 0;
|
|
2370
|
+
let currentRoundType = "HomeAndAway";
|
|
2371
|
+
let matchCounter = 0;
|
|
2372
|
+
$("table").each((_i, table) => {
|
|
2373
|
+
const $table = $(table);
|
|
2374
|
+
const text = $table.text().trim();
|
|
2375
|
+
const border = $table.attr("border");
|
|
2376
|
+
const roundMatch = /^Round\s+(\d+)/i.exec(text);
|
|
2377
|
+
if (roundMatch?.[1] && border !== "1") {
|
|
2378
|
+
currentRound = Number.parseInt(roundMatch[1], 10);
|
|
2379
|
+
currentRoundType = inferRoundType(text);
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (border !== "1" && inferRoundType(text) === "Finals") {
|
|
2383
|
+
currentRoundType = "Finals";
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
if (border !== "1") return;
|
|
2387
|
+
const rows = $table.find("tr");
|
|
2388
|
+
if (rows.length !== 2) return;
|
|
2389
|
+
const homeRow = $(rows[0]);
|
|
2390
|
+
const awayRow = $(rows[1]);
|
|
2391
|
+
const homeCells = homeRow.find("td");
|
|
2392
|
+
const awayCells = awayRow.find("td");
|
|
2393
|
+
if (homeCells.length < 4 || awayCells.length < 3) return;
|
|
2394
|
+
const homeTeam = normaliseTeamName($(homeCells[0]).find("a").text().trim());
|
|
2395
|
+
const awayTeam = normaliseTeamName($(awayCells[0]).find("a").text().trim());
|
|
2396
|
+
if (!homeTeam || !awayTeam) return;
|
|
2397
|
+
const homeQuarters = parseQuarterScores($(homeCells[1]).text());
|
|
2398
|
+
const awayQuarters = parseQuarterScores($(awayCells[1]).text());
|
|
2399
|
+
const homePoints = Number.parseInt($(homeCells[2]).text().trim(), 10) || 0;
|
|
2400
|
+
const awayPoints = Number.parseInt($(awayCells[2]).text().trim(), 10) || 0;
|
|
2401
|
+
const infoText = $(homeCells[3]).text().trim();
|
|
2402
|
+
const date = parseDateFromInfo(infoText, year);
|
|
2403
|
+
const venue = parseVenueFromInfo($(homeCells[3]).html() ?? "");
|
|
2404
|
+
const attendance = parseAttendanceFromInfo(infoText);
|
|
2405
|
+
const homeFinal = homeQuarters[3];
|
|
2406
|
+
const awayFinal = awayQuarters[3];
|
|
2407
|
+
matchCounter++;
|
|
2408
|
+
results.push({
|
|
2409
|
+
matchId: `AT_${year}_${matchCounter}`,
|
|
2410
|
+
season: year,
|
|
2411
|
+
roundNumber: currentRound,
|
|
2412
|
+
roundType: currentRoundType,
|
|
2413
|
+
date,
|
|
2414
|
+
venue,
|
|
2415
|
+
homeTeam,
|
|
2416
|
+
awayTeam,
|
|
2417
|
+
homeGoals: homeFinal?.goals ?? 0,
|
|
2418
|
+
homeBehinds: homeFinal?.behinds ?? 0,
|
|
2419
|
+
homePoints,
|
|
2420
|
+
awayGoals: awayFinal?.goals ?? 0,
|
|
2421
|
+
awayBehinds: awayFinal?.behinds ?? 0,
|
|
2422
|
+
awayPoints,
|
|
2423
|
+
margin: homePoints - awayPoints,
|
|
2424
|
+
q1Home: homeQuarters[0] ?? null,
|
|
2425
|
+
q2Home: homeQuarters[1] ?? null,
|
|
2426
|
+
q3Home: homeQuarters[2] ?? null,
|
|
2427
|
+
q4Home: homeQuarters[3] ?? null,
|
|
2428
|
+
q1Away: awayQuarters[0] ?? null,
|
|
2429
|
+
q2Away: awayQuarters[1] ?? null,
|
|
2430
|
+
q3Away: awayQuarters[2] ?? null,
|
|
2431
|
+
q4Away: awayQuarters[3] ?? null,
|
|
2432
|
+
status: "Complete",
|
|
2433
|
+
attendance,
|
|
2434
|
+
venueState: null,
|
|
2435
|
+
venueTimezone: null,
|
|
2436
|
+
homeRushedBehinds: null,
|
|
2437
|
+
awayRushedBehinds: null,
|
|
2438
|
+
homeMinutesInFront: null,
|
|
2439
|
+
awayMinutesInFront: null,
|
|
2440
|
+
source: "afl-tables",
|
|
2441
|
+
competition: "AFLM"
|
|
2442
|
+
});
|
|
2443
|
+
});
|
|
2444
|
+
return results;
|
|
2445
|
+
}
|
|
2446
|
+
function parseQuarterScores(text) {
|
|
2447
|
+
const clean = text.replace(/\u00a0/g, " ").trim();
|
|
2448
|
+
const matches = [...clean.matchAll(/(\d+)\.(\d+)/g)];
|
|
2449
|
+
return matches.map((m) => {
|
|
2450
|
+
const goals = Number.parseInt(m[1] ?? "0", 10);
|
|
2451
|
+
const behinds = Number.parseInt(m[2] ?? "0", 10);
|
|
2452
|
+
return { goals, behinds, points: goals * 6 + behinds };
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
function parseDateFromInfo(text, year) {
|
|
2456
|
+
const dateMatch = /(\d{1,2}-[A-Z][a-z]{2}-\d{4})/.exec(text);
|
|
2457
|
+
if (dateMatch?.[1]) {
|
|
2458
|
+
return parseAflTablesDate(dateMatch[1]) ?? new Date(year, 0, 1);
|
|
2459
|
+
}
|
|
2460
|
+
return parseAflTablesDate(text) ?? new Date(year, 0, 1);
|
|
2461
|
+
}
|
|
2462
|
+
function parseVenueFromInfo(html) {
|
|
2463
|
+
const $ = cheerio5.load(html);
|
|
2464
|
+
const venueLink = $("a[href*='venues']");
|
|
2465
|
+
if (venueLink.length > 0) {
|
|
2466
|
+
return venueLink.text().trim();
|
|
2467
|
+
}
|
|
2468
|
+
const venueMatch = /Venue:\s*(.+?)(?:<|$)/i.exec(html);
|
|
2469
|
+
return venueMatch?.[1]?.trim() ?? "";
|
|
2470
|
+
}
|
|
2471
|
+
function parseAttendanceFromInfo(text) {
|
|
2472
|
+
const match = /Att:\s*([\d,]+)/i.exec(text);
|
|
2473
|
+
if (!match?.[1]) return null;
|
|
2474
|
+
return Number.parseInt(match[1].replace(/,/g, ""), 10) || null;
|
|
2475
|
+
}
|
|
2476
|
+
function parseAflTablesTeamStats(html, year) {
|
|
2477
|
+
const $ = cheerio5.load(html);
|
|
2478
|
+
const teamMap = /* @__PURE__ */ new Map();
|
|
2479
|
+
const tables = $("table");
|
|
2480
|
+
function parseTable(tableIdx, suffix) {
|
|
2481
|
+
if (tableIdx >= tables.length) return;
|
|
2482
|
+
const $table = $(tables[tableIdx]);
|
|
2483
|
+
const rows = $table.find("tr");
|
|
2484
|
+
if (rows.length < 2) return;
|
|
2485
|
+
const headers = [];
|
|
2486
|
+
$(rows[0]).find("td, th").each((_ci, cell) => {
|
|
2487
|
+
headers.push($(cell).text().trim());
|
|
2488
|
+
});
|
|
2489
|
+
for (let ri = 1; ri < rows.length; ri++) {
|
|
2490
|
+
const cells = $(rows[ri]).find("td");
|
|
2491
|
+
if (cells.length < 3) continue;
|
|
2492
|
+
const teamText = $(cells[0]).text().trim();
|
|
2493
|
+
if (teamText === "Totals" || !teamText) continue;
|
|
2494
|
+
const teamName = normaliseTeamName(teamText);
|
|
2495
|
+
if (!teamName) continue;
|
|
2496
|
+
if (!teamMap.has(teamName)) {
|
|
2497
|
+
teamMap.set(teamName, { gamesPlayed: 0, stats: {} });
|
|
2498
|
+
}
|
|
2499
|
+
const entry = teamMap.get(teamName);
|
|
2500
|
+
if (!entry) continue;
|
|
2501
|
+
for (let ci = 1; ci < cells.length; ci++) {
|
|
2502
|
+
const header = headers[ci];
|
|
2503
|
+
if (!header) continue;
|
|
2504
|
+
const value = Number.parseFloat($(cells[ci]).text().trim().replace(/,/g, "")) || 0;
|
|
2505
|
+
entry.stats[`${header}${suffix}`] = value;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
parseTable(1, "_for");
|
|
2510
|
+
parseTable(2, "_against");
|
|
2511
|
+
const entries = [];
|
|
2512
|
+
for (const [team, data] of teamMap) {
|
|
2513
|
+
entries.push({
|
|
2514
|
+
season: year,
|
|
2515
|
+
team,
|
|
2516
|
+
gamesPlayed: data.gamesPlayed,
|
|
2517
|
+
stats: data.stats,
|
|
2518
|
+
source: "afl-tables"
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
return entries;
|
|
2522
|
+
}
|
|
2523
|
+
function teamNameToAflTablesSlug(teamName) {
|
|
2524
|
+
return AFL_TABLES_SLUG_MAP.get(teamName);
|
|
2525
|
+
}
|
|
2526
|
+
function parseAflTablesPlayerList(html, teamName) {
|
|
2527
|
+
const $ = cheerio5.load(html);
|
|
2528
|
+
const players = [];
|
|
2529
|
+
const table = $("table.sortable").first();
|
|
2530
|
+
if (table.length === 0) return players;
|
|
2531
|
+
const rows = table.find("tbody tr");
|
|
2532
|
+
rows.each((_ri, row) => {
|
|
2533
|
+
const cells = $(row).find("td");
|
|
2534
|
+
if (cells.length < 8) return;
|
|
2535
|
+
const jumperText = $(cells[1]).text().trim();
|
|
2536
|
+
const playerText = $(cells[2]).text().trim();
|
|
2537
|
+
if (!playerText) return;
|
|
2538
|
+
const nameParts = playerText.split(",").map((s) => s.trim());
|
|
2539
|
+
const surname = nameParts[0] ?? "";
|
|
2540
|
+
const givenName = nameParts[1] ?? "";
|
|
2541
|
+
const dobText = $(cells[3]).text().trim();
|
|
2542
|
+
const htText = $(cells[4]).text().trim();
|
|
2543
|
+
const wtText = $(cells[5]).text().trim();
|
|
2544
|
+
const gamesRaw = $(cells[6]).text().trim();
|
|
2545
|
+
const gamesMatch = /^(\d+)/.exec(gamesRaw);
|
|
2546
|
+
const goalsText = $(cells[7]).text().trim();
|
|
2547
|
+
const debutText = cells.length > 9 ? $(cells[9]).text().trim() : "";
|
|
2548
|
+
const heightCm = htText ? Number.parseInt(htText, 10) || null : null;
|
|
2549
|
+
const weightKg = wtText ? Number.parseInt(wtText, 10) || null : null;
|
|
2550
|
+
const gamesPlayed = gamesMatch?.[1] ? Number.parseInt(gamesMatch[1], 10) || null : null;
|
|
2551
|
+
const goalsScored = goalsText ? Number.parseInt(goalsText, 10) || null : null;
|
|
2552
|
+
const jumperNumber = jumperText ? Number.parseInt(jumperText, 10) || null : null;
|
|
2553
|
+
const debutYearMatch = /(\d{4})/.exec(debutText);
|
|
2554
|
+
const debutYear = debutYearMatch?.[1] ? Number.parseInt(debutYearMatch[1], 10) || null : null;
|
|
2555
|
+
players.push({
|
|
2556
|
+
playerId: `AT_${teamName}_${surname}_${givenName}`.replace(/\s+/g, "_"),
|
|
2557
|
+
givenName,
|
|
2558
|
+
surname,
|
|
2559
|
+
displayName: givenName ? `${givenName} ${surname}` : surname,
|
|
2560
|
+
team: teamName,
|
|
2561
|
+
jumperNumber,
|
|
2562
|
+
position: null,
|
|
2563
|
+
dateOfBirth: dobText || null,
|
|
2564
|
+
heightCm,
|
|
2565
|
+
weightKg,
|
|
2566
|
+
gamesPlayed,
|
|
2567
|
+
goals: goalsScored,
|
|
2568
|
+
draftYear: null,
|
|
2569
|
+
draftPosition: null,
|
|
2570
|
+
draftType: null,
|
|
2571
|
+
debutYear,
|
|
2572
|
+
recruitedFrom: null
|
|
2573
|
+
});
|
|
2574
|
+
});
|
|
2575
|
+
return players;
|
|
2576
|
+
}
|
|
2577
|
+
var AFL_TABLES_BASE, AflTablesClient, AFL_TABLES_SLUG_MAP;
|
|
2578
|
+
var init_afl_tables = __esm({
|
|
2579
|
+
"src/sources/afl-tables.ts"() {
|
|
2580
|
+
"use strict";
|
|
2581
|
+
init_date_utils();
|
|
2582
|
+
init_errors();
|
|
2583
|
+
init_result();
|
|
2584
|
+
init_team_mapping();
|
|
2585
|
+
init_afl_tables_player_stats();
|
|
2586
|
+
init_match_results();
|
|
2587
|
+
AFL_TABLES_BASE = "https://afltables.com/afl/seas";
|
|
2588
|
+
AflTablesClient = class {
|
|
2589
|
+
fetchFn;
|
|
2590
|
+
constructor(options) {
|
|
2591
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Fetch season match results from AFL Tables.
|
|
2595
|
+
*
|
|
2596
|
+
* @param year - The season year (1897 to present).
|
|
2597
|
+
* @returns Array of match results.
|
|
2598
|
+
*/
|
|
2599
|
+
async fetchSeasonResults(year) {
|
|
2600
|
+
const url = `${AFL_TABLES_BASE}/${year}.html`;
|
|
2601
|
+
try {
|
|
2602
|
+
const response = await this.fetchFn(url, {
|
|
2603
|
+
headers: { "User-Agent": "Mozilla/5.0" }
|
|
2604
|
+
});
|
|
2605
|
+
if (!response.ok) {
|
|
2606
|
+
return err(
|
|
2607
|
+
new ScrapeError(`AFL Tables request failed: ${response.status} (${url})`, "afl-tables")
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
const html = await response.text();
|
|
2611
|
+
const results = parseSeasonPage(html, year);
|
|
2612
|
+
return ok(results);
|
|
2613
|
+
} catch (cause) {
|
|
2614
|
+
return err(
|
|
2615
|
+
new ScrapeError(
|
|
2616
|
+
`AFL Tables request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
2617
|
+
"afl-tables"
|
|
2618
|
+
)
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Fetch player statistics for an entire season from AFL Tables.
|
|
2624
|
+
*
|
|
2625
|
+
* Scrapes individual game pages linked from the season page.
|
|
2626
|
+
*
|
|
2627
|
+
* @param year - The season year.
|
|
2628
|
+
*/
|
|
2629
|
+
async fetchSeasonPlayerStats(year) {
|
|
2630
|
+
const seasonUrl = `${AFL_TABLES_BASE}/${year}.html`;
|
|
2631
|
+
try {
|
|
2632
|
+
const seasonResponse = await this.fetchFn(seasonUrl, {
|
|
2633
|
+
headers: { "User-Agent": "Mozilla/5.0" }
|
|
2634
|
+
});
|
|
2635
|
+
if (!seasonResponse.ok) {
|
|
2636
|
+
return err(
|
|
2637
|
+
new ScrapeError(
|
|
2638
|
+
`AFL Tables request failed: ${seasonResponse.status} (${seasonUrl})`,
|
|
2639
|
+
"afl-tables"
|
|
2640
|
+
)
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
const seasonHtml = await seasonResponse.text();
|
|
2644
|
+
const gameUrls = extractGameUrls(seasonHtml);
|
|
2645
|
+
if (gameUrls.length === 0) {
|
|
2646
|
+
return ok([]);
|
|
2647
|
+
}
|
|
2648
|
+
const results = parseSeasonPage(seasonHtml, year);
|
|
2649
|
+
const allStats = [];
|
|
2650
|
+
const batchSize = 5;
|
|
2651
|
+
for (let i = 0; i < gameUrls.length; i += batchSize) {
|
|
2652
|
+
const batch = gameUrls.slice(i, i + batchSize);
|
|
2653
|
+
const batchResults = await Promise.all(
|
|
2654
|
+
batch.map(async (gameUrl, batchIdx) => {
|
|
2655
|
+
try {
|
|
2656
|
+
const resp = await this.fetchFn(gameUrl, {
|
|
2657
|
+
headers: { "User-Agent": "Mozilla/5.0" }
|
|
2658
|
+
});
|
|
2659
|
+
if (!resp.ok) return [];
|
|
2660
|
+
const html = await resp.text();
|
|
2661
|
+
const urlMatch = /\/(\d+)\.html$/.exec(gameUrl);
|
|
2662
|
+
const matchId = urlMatch?.[1] ?? `${year}_${i + batchIdx}`;
|
|
2663
|
+
const globalIdx = i + batchIdx;
|
|
2664
|
+
const roundNumber = results[globalIdx]?.roundNumber ?? 0;
|
|
2665
|
+
return parseAflTablesGameStats(html, matchId, year, roundNumber);
|
|
2666
|
+
} catch {
|
|
2667
|
+
return [];
|
|
2668
|
+
}
|
|
2669
|
+
})
|
|
2670
|
+
);
|
|
2671
|
+
for (const stats of batchResults) {
|
|
2672
|
+
allStats.push(...stats);
|
|
2673
|
+
}
|
|
2674
|
+
if (i + batchSize < gameUrls.length) {
|
|
2675
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
return ok(allStats);
|
|
2679
|
+
} catch (cause) {
|
|
2680
|
+
return err(
|
|
2681
|
+
new ScrapeError(
|
|
2682
|
+
`AFL Tables player stats failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
2683
|
+
"afl-tables"
|
|
2684
|
+
)
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Fetch team statistics from AFL Tables.
|
|
2690
|
+
*
|
|
2691
|
+
* Scrapes the season stats page which includes per-team aggregate stats.
|
|
2692
|
+
*
|
|
2693
|
+
* @param year - The season year.
|
|
2694
|
+
* @returns Array of team stats entries.
|
|
2695
|
+
*/
|
|
2696
|
+
async fetchTeamStats(year) {
|
|
2697
|
+
const url = `https://afltables.com/afl/stats/${year}s.html`;
|
|
2698
|
+
try {
|
|
2699
|
+
const response = await this.fetchFn(url, {
|
|
2700
|
+
headers: { "User-Agent": "Mozilla/5.0" }
|
|
2701
|
+
});
|
|
2702
|
+
if (!response.ok) {
|
|
2703
|
+
return err(
|
|
2704
|
+
new ScrapeError(
|
|
2705
|
+
`AFL Tables stats request failed: ${response.status} (${url})`,
|
|
2706
|
+
"afl-tables"
|
|
2707
|
+
)
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
const html = await response.text();
|
|
2711
|
+
const entries = parseAflTablesTeamStats(html, year);
|
|
2712
|
+
return ok(entries);
|
|
2713
|
+
} catch (cause) {
|
|
2714
|
+
return err(
|
|
2715
|
+
new ScrapeError(
|
|
2716
|
+
`AFL Tables team stats failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
2717
|
+
"afl-tables"
|
|
2718
|
+
)
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Fetch player list from AFL Tables team page.
|
|
2724
|
+
*
|
|
2725
|
+
* Scrapes the team index page (e.g. `teams/swans_idx.html`) which lists
|
|
2726
|
+
* all players who have played for that team historically.
|
|
2727
|
+
*
|
|
2728
|
+
* @param teamName - Canonical team name (e.g. "Sydney Swans").
|
|
2729
|
+
* @returns Array of player details (without source/competition fields).
|
|
2730
|
+
*/
|
|
2731
|
+
async fetchPlayerList(teamName) {
|
|
2732
|
+
const slug = teamNameToAflTablesSlug(teamName);
|
|
2733
|
+
if (!slug) {
|
|
2734
|
+
return err(new ScrapeError(`No AFL Tables slug mapping for team: ${teamName}`, "afl-tables"));
|
|
2735
|
+
}
|
|
2736
|
+
const url = `https://afltables.com/afl/stats/alltime/${slug}.html`;
|
|
2737
|
+
try {
|
|
2738
|
+
const response = await this.fetchFn(url, {
|
|
2739
|
+
headers: { "User-Agent": "Mozilla/5.0" }
|
|
2740
|
+
});
|
|
2741
|
+
if (!response.ok) {
|
|
2742
|
+
return err(
|
|
2743
|
+
new ScrapeError(`AFL Tables request failed: ${response.status} (${url})`, "afl-tables")
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
const html = await response.text();
|
|
2747
|
+
const players = parseAflTablesPlayerList(html, teamName);
|
|
2748
|
+
return ok(players);
|
|
2749
|
+
} catch (cause) {
|
|
2750
|
+
return err(
|
|
2751
|
+
new ScrapeError(
|
|
2752
|
+
`AFL Tables player list failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
2753
|
+
"afl-tables"
|
|
2754
|
+
)
|
|
2755
|
+
);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
};
|
|
2759
|
+
AFL_TABLES_SLUG_MAP = /* @__PURE__ */ new Map([
|
|
2760
|
+
["Adelaide Crows", "adelaide"],
|
|
2761
|
+
["Brisbane Lions", "brisbane"],
|
|
2762
|
+
["Carlton", "carlton"],
|
|
2763
|
+
["Collingwood", "collingwood"],
|
|
2764
|
+
["Essendon", "essendon"],
|
|
2765
|
+
["Fremantle", "fremantle"],
|
|
2766
|
+
["Geelong Cats", "geelong"],
|
|
2767
|
+
["Gold Coast Suns", "goldcoast"],
|
|
2768
|
+
["GWS Giants", "gws"],
|
|
2769
|
+
["Hawthorn", "hawthorn"],
|
|
2770
|
+
["Melbourne", "melbourne"],
|
|
2771
|
+
["North Melbourne", "kangaroos"],
|
|
2772
|
+
["Port Adelaide", "padelaide"],
|
|
2773
|
+
["Richmond", "richmond"],
|
|
2774
|
+
["St Kilda", "stkilda"],
|
|
2775
|
+
["Sydney Swans", "swans"],
|
|
2776
|
+
["West Coast Eagles", "westcoast"],
|
|
2777
|
+
["Western Bulldogs", "bullldogs"],
|
|
2778
|
+
["Fitzroy", "fitzroy"],
|
|
2779
|
+
["University", "university"]
|
|
2780
|
+
]);
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
// src/transforms/computed-ladder.ts
|
|
2785
|
+
function computeLadder(results, upToRound) {
|
|
2786
|
+
const teams = /* @__PURE__ */ new Map();
|
|
2787
|
+
const filtered = upToRound != null ? results.filter((r) => r.roundType === "HomeAndAway" && r.roundNumber <= upToRound) : results.filter((r) => r.roundType === "HomeAndAway");
|
|
2788
|
+
for (const match of filtered) {
|
|
2789
|
+
if (match.status !== "Complete") continue;
|
|
2790
|
+
const home = getOrCreate(teams, match.homeTeam);
|
|
2791
|
+
const away = getOrCreate(teams, match.awayTeam);
|
|
2792
|
+
home.played++;
|
|
2793
|
+
away.played++;
|
|
2794
|
+
home.pointsFor += match.homePoints;
|
|
2795
|
+
home.pointsAgainst += match.awayPoints;
|
|
2796
|
+
away.pointsFor += match.awayPoints;
|
|
2797
|
+
away.pointsAgainst += match.homePoints;
|
|
2798
|
+
if (match.homePoints > match.awayPoints) {
|
|
2799
|
+
home.wins++;
|
|
2800
|
+
away.losses++;
|
|
2801
|
+
} else if (match.awayPoints > match.homePoints) {
|
|
2802
|
+
away.wins++;
|
|
2803
|
+
home.losses++;
|
|
2804
|
+
} else {
|
|
2805
|
+
home.draws++;
|
|
2806
|
+
away.draws++;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
const entries = [...teams.entries()].map(([teamName, acc]) => {
|
|
2810
|
+
const percentage = acc.pointsAgainst === 0 ? 0 : acc.pointsFor / acc.pointsAgainst * 100;
|
|
2811
|
+
const premiershipsPoints = acc.wins * 4 + acc.draws * 2;
|
|
2812
|
+
return {
|
|
2813
|
+
position: 0,
|
|
2814
|
+
// filled below after sorting
|
|
2815
|
+
team: teamName,
|
|
2816
|
+
played: acc.played,
|
|
2817
|
+
wins: acc.wins,
|
|
2818
|
+
losses: acc.losses,
|
|
2819
|
+
draws: acc.draws,
|
|
2820
|
+
pointsFor: acc.pointsFor,
|
|
2821
|
+
pointsAgainst: acc.pointsAgainst,
|
|
2822
|
+
percentage,
|
|
2823
|
+
premiershipsPoints,
|
|
2824
|
+
form: null
|
|
2825
|
+
};
|
|
2826
|
+
});
|
|
2827
|
+
entries.sort((a, b) => {
|
|
2828
|
+
if (b.premiershipsPoints !== a.premiershipsPoints) {
|
|
2829
|
+
return b.premiershipsPoints - a.premiershipsPoints;
|
|
2830
|
+
}
|
|
2831
|
+
return b.percentage - a.percentage;
|
|
2832
|
+
});
|
|
2833
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2834
|
+
const entry = entries[i];
|
|
2835
|
+
if (entry) {
|
|
2836
|
+
entries[i] = { ...entry, position: i + 1 };
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
return entries;
|
|
2840
|
+
}
|
|
2841
|
+
function getOrCreate(map, team) {
|
|
2842
|
+
let acc = map.get(team);
|
|
2843
|
+
if (!acc) {
|
|
2844
|
+
acc = { played: 0, wins: 0, losses: 0, draws: 0, pointsFor: 0, pointsAgainst: 0 };
|
|
2845
|
+
map.set(team, acc);
|
|
2846
|
+
}
|
|
2847
|
+
return acc;
|
|
2848
|
+
}
|
|
2849
|
+
var init_computed_ladder = __esm({
|
|
2850
|
+
"src/transforms/computed-ladder.ts"() {
|
|
2851
|
+
"use strict";
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
|
|
2855
|
+
// src/transforms/ladder.ts
|
|
2856
|
+
function transformLadderEntries(entries) {
|
|
2857
|
+
return entries.map((entry) => {
|
|
2858
|
+
const record = entry.thisSeasonRecord;
|
|
2859
|
+
const wl = record?.winLossRecord;
|
|
2860
|
+
return {
|
|
2861
|
+
position: entry.position,
|
|
2862
|
+
team: normaliseTeamName(entry.team.name),
|
|
2863
|
+
played: entry.played ?? wl?.played ?? 0,
|
|
2864
|
+
wins: wl?.wins ?? 0,
|
|
2865
|
+
losses: wl?.losses ?? 0,
|
|
2866
|
+
draws: wl?.draws ?? 0,
|
|
2867
|
+
pointsFor: entry.pointsFor ?? 0,
|
|
2868
|
+
pointsAgainst: entry.pointsAgainst ?? 0,
|
|
2869
|
+
percentage: record?.percentage ?? 0,
|
|
2870
|
+
premiershipsPoints: record?.aggregatePoints ?? 0,
|
|
2871
|
+
form: entry.form ?? null
|
|
2872
|
+
};
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
var init_ladder = __esm({
|
|
2876
|
+
"src/transforms/ladder.ts"() {
|
|
2877
|
+
"use strict";
|
|
2878
|
+
init_team_mapping();
|
|
2879
|
+
}
|
|
2880
|
+
});
|
|
2881
|
+
|
|
2882
|
+
// src/api/ladder.ts
|
|
2883
|
+
async function fetchLadder(query) {
|
|
2884
|
+
const competition = query.competition ?? "AFLM";
|
|
2885
|
+
if (query.source === "squiggle") {
|
|
2886
|
+
const client2 = new SquiggleClient();
|
|
2887
|
+
const result = await client2.fetchStandings(query.season, query.round ?? void 0);
|
|
2888
|
+
if (!result.success) return result;
|
|
2889
|
+
return ok({
|
|
2890
|
+
season: query.season,
|
|
2891
|
+
roundNumber: query.round ?? null,
|
|
2892
|
+
entries: transformSquiggleStandings(result.data.standings),
|
|
2893
|
+
competition
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
if (query.source === "afl-tables") {
|
|
2897
|
+
const atClient = new AflTablesClient();
|
|
2898
|
+
const resultsResult = await atClient.fetchSeasonResults(query.season);
|
|
2899
|
+
if (!resultsResult.success) return resultsResult;
|
|
2900
|
+
const entries2 = computeLadder(resultsResult.data, query.round ?? void 0);
|
|
2901
|
+
return ok({
|
|
2902
|
+
season: query.season,
|
|
2903
|
+
roundNumber: query.round ?? null,
|
|
2904
|
+
entries: entries2,
|
|
2905
|
+
competition
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
if (query.source !== "afl-api") {
|
|
2909
|
+
return err(
|
|
2910
|
+
new UnsupportedSourceError(
|
|
2911
|
+
"Ladder data is only available from the AFL API, AFL Tables, or Squiggle sources.",
|
|
2912
|
+
query.source
|
|
2913
|
+
)
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
const client = new AflApiClient();
|
|
2917
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
2918
|
+
if (!seasonResult.success) return seasonResult;
|
|
2919
|
+
let roundId;
|
|
2920
|
+
if (query.round != null) {
|
|
2921
|
+
const roundsResult = await client.resolveRounds(seasonResult.data);
|
|
2922
|
+
if (!roundsResult.success) return roundsResult;
|
|
2923
|
+
const round = roundsResult.data.find((r) => r.roundNumber === query.round);
|
|
2924
|
+
if (round) {
|
|
2925
|
+
roundId = round.id;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
const ladderResult = await client.fetchLadder(seasonResult.data, roundId);
|
|
2929
|
+
if (!ladderResult.success) return ladderResult;
|
|
2930
|
+
const firstLadder = ladderResult.data.ladders[0];
|
|
2931
|
+
const entries = firstLadder ? transformLadderEntries(firstLadder.entries) : [];
|
|
2932
|
+
return ok({
|
|
2933
|
+
season: query.season,
|
|
2934
|
+
roundNumber: ladderResult.data.round?.roundNumber ?? null,
|
|
2935
|
+
entries,
|
|
2936
|
+
competition
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
var init_ladder2 = __esm({
|
|
2940
|
+
"src/api/ladder.ts"() {
|
|
2941
|
+
"use strict";
|
|
2942
|
+
init_errors();
|
|
2943
|
+
init_result();
|
|
2944
|
+
init_afl_api();
|
|
2945
|
+
init_afl_tables();
|
|
2946
|
+
init_squiggle();
|
|
2947
|
+
init_computed_ladder();
|
|
2948
|
+
init_ladder();
|
|
2949
|
+
init_squiggle2();
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
|
|
2953
|
+
// src/transforms/lineup.ts
|
|
2954
|
+
function transformMatchRoster(roster, season, roundNumber, competition) {
|
|
2955
|
+
const homeTeamId = roster.match.homeTeamId;
|
|
2956
|
+
const awayTeamId = roster.match.awayTeamId;
|
|
2957
|
+
const homeTeamPlayers = roster.teamPlayers.find((tp) => tp.teamId === homeTeamId);
|
|
2958
|
+
const awayTeamPlayers = roster.teamPlayers.find((tp) => tp.teamId === awayTeamId);
|
|
2959
|
+
const mapPlayers = (players) => players.map((p) => {
|
|
2960
|
+
const inner = p.player.player;
|
|
2961
|
+
const position = p.player.position ?? null;
|
|
2962
|
+
return {
|
|
2963
|
+
playerId: inner.playerId,
|
|
2964
|
+
givenName: inner.playerName.givenName,
|
|
2965
|
+
surname: inner.playerName.surname,
|
|
2966
|
+
displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
|
|
2967
|
+
jumperNumber: p.jumperNumber ?? null,
|
|
2968
|
+
position,
|
|
2969
|
+
isEmergency: position !== null && EMERGENCY_POSITIONS.has(position),
|
|
2970
|
+
isSubstitute: position !== null && SUBSTITUTE_POSITIONS.has(position)
|
|
2971
|
+
};
|
|
2972
|
+
});
|
|
2973
|
+
return {
|
|
2974
|
+
matchId: roster.match.matchId,
|
|
2975
|
+
season,
|
|
2976
|
+
roundNumber,
|
|
2977
|
+
homeTeam: normaliseTeamName(roster.match.homeTeam.name),
|
|
2978
|
+
awayTeam: normaliseTeamName(roster.match.awayTeam.name),
|
|
2979
|
+
homePlayers: homeTeamPlayers ? mapPlayers(homeTeamPlayers.players) : [],
|
|
2980
|
+
awayPlayers: awayTeamPlayers ? mapPlayers(awayTeamPlayers.players) : [],
|
|
2981
|
+
competition
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
var EMERGENCY_POSITIONS, SUBSTITUTE_POSITIONS;
|
|
2985
|
+
var init_lineup = __esm({
|
|
2986
|
+
"src/transforms/lineup.ts"() {
|
|
2987
|
+
"use strict";
|
|
2988
|
+
init_team_mapping();
|
|
2989
|
+
EMERGENCY_POSITIONS = /* @__PURE__ */ new Set(["EMG", "EMERG"]);
|
|
2990
|
+
SUBSTITUTE_POSITIONS = /* @__PURE__ */ new Set(["SUB", "INT"]);
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
|
|
2994
|
+
// src/api/lineup.ts
|
|
2995
|
+
async function fetchLineup(query) {
|
|
2996
|
+
const competition = query.competition ?? "AFLM";
|
|
2997
|
+
if (query.source !== "afl-api") {
|
|
2998
|
+
return err(
|
|
2999
|
+
new UnsupportedSourceError(
|
|
3000
|
+
"Lineup data is only available from the AFL API source.",
|
|
3001
|
+
query.source
|
|
3002
|
+
)
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
3005
|
+
const client = new AflApiClient();
|
|
3006
|
+
if (query.matchId) {
|
|
3007
|
+
const rosterResult = await client.fetchMatchRoster(query.matchId);
|
|
3008
|
+
if (!rosterResult.success) return rosterResult;
|
|
3009
|
+
return ok([transformMatchRoster(rosterResult.data, query.season, query.round, competition)]);
|
|
3010
|
+
}
|
|
3011
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
3012
|
+
if (!seasonResult.success) return seasonResult;
|
|
3013
|
+
const matchItems = await client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round);
|
|
3014
|
+
if (!matchItems.success) return matchItems;
|
|
3015
|
+
if (matchItems.data.length === 0) {
|
|
3016
|
+
return err(new AflApiError(`No matches found for round ${query.round}`));
|
|
3017
|
+
}
|
|
3018
|
+
const rosterResults = await Promise.all(
|
|
3019
|
+
matchItems.data.map((item) => client.fetchMatchRoster(item.match.matchId))
|
|
3020
|
+
);
|
|
3021
|
+
const lineups = [];
|
|
3022
|
+
for (const rosterResult of rosterResults) {
|
|
3023
|
+
if (!rosterResult.success) return rosterResult;
|
|
3024
|
+
lineups.push(transformMatchRoster(rosterResult.data, query.season, query.round, competition));
|
|
3025
|
+
}
|
|
3026
|
+
return ok(lineups);
|
|
3027
|
+
}
|
|
3028
|
+
var init_lineup2 = __esm({
|
|
3029
|
+
"src/api/lineup.ts"() {
|
|
3030
|
+
"use strict";
|
|
3031
|
+
init_errors();
|
|
3032
|
+
init_result();
|
|
3033
|
+
init_afl_api();
|
|
3034
|
+
init_lineup();
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
3037
|
+
|
|
3038
|
+
// src/api/match-results.ts
|
|
3039
|
+
async function fetchMatchResults(query) {
|
|
3040
|
+
const competition = query.competition ?? "AFLM";
|
|
3041
|
+
switch (query.source) {
|
|
3042
|
+
case "afl-api": {
|
|
3043
|
+
const client = new AflApiClient();
|
|
3044
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
3045
|
+
if (!seasonResult.success) return seasonResult;
|
|
3046
|
+
if (query.round != null) {
|
|
3047
|
+
const itemsResult2 = await client.fetchRoundMatchItemsByNumber(
|
|
3048
|
+
seasonResult.data,
|
|
3049
|
+
query.round
|
|
3050
|
+
);
|
|
3051
|
+
if (!itemsResult2.success) return itemsResult2;
|
|
3052
|
+
return ok(transformMatchItems(itemsResult2.data, query.season, competition));
|
|
3053
|
+
}
|
|
3054
|
+
const itemsResult = await client.fetchSeasonMatchItems(seasonResult.data);
|
|
3055
|
+
if (!itemsResult.success) return itemsResult;
|
|
3056
|
+
return ok(transformMatchItems(itemsResult.data, query.season, competition));
|
|
3057
|
+
}
|
|
3058
|
+
case "footywire": {
|
|
3059
|
+
const client = new FootyWireClient();
|
|
3060
|
+
const result = await client.fetchSeasonResults(query.season);
|
|
3061
|
+
if (!result.success) return result;
|
|
3062
|
+
if (query.round != null) {
|
|
3063
|
+
return ok(result.data.filter((m) => m.roundNumber === query.round));
|
|
3064
|
+
}
|
|
3065
|
+
return result;
|
|
3066
|
+
}
|
|
3067
|
+
case "afl-tables": {
|
|
3068
|
+
const client = new AflTablesClient();
|
|
3069
|
+
const result = await client.fetchSeasonResults(query.season);
|
|
3070
|
+
if (!result.success) return result;
|
|
3071
|
+
if (query.round != null) {
|
|
3072
|
+
return ok(result.data.filter((m) => m.roundNumber === query.round));
|
|
3073
|
+
}
|
|
3074
|
+
return result;
|
|
3075
|
+
}
|
|
3076
|
+
case "squiggle": {
|
|
3077
|
+
const client = new SquiggleClient();
|
|
3078
|
+
const result = await client.fetchGames(query.season, query.round ?? void 0, 100);
|
|
3079
|
+
if (!result.success) return result;
|
|
3080
|
+
return ok(transformSquiggleGamesToResults(result.data.games, query.season));
|
|
3081
|
+
}
|
|
3082
|
+
default:
|
|
3083
|
+
return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
var init_match_results2 = __esm({
|
|
3087
|
+
"src/api/match-results.ts"() {
|
|
3088
|
+
"use strict";
|
|
3089
|
+
init_errors();
|
|
3090
|
+
init_result();
|
|
3091
|
+
init_afl_api();
|
|
3092
|
+
init_afl_tables();
|
|
3093
|
+
init_footywire();
|
|
3094
|
+
init_squiggle();
|
|
3095
|
+
init_match_results();
|
|
3096
|
+
init_squiggle2();
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
|
|
3100
|
+
// src/api/player-details.ts
|
|
3101
|
+
async function resolveTeamId(client, teamName, competition) {
|
|
3102
|
+
const teamType = competition === "AFLW" ? "WOMEN" : "MEN";
|
|
3103
|
+
const result = await client.fetchTeams(teamType);
|
|
3104
|
+
if (!result.success) return result;
|
|
3105
|
+
const normalised = normaliseTeamName(teamName);
|
|
3106
|
+
const match = result.data.find((t) => normaliseTeamName(t.name) === normalised);
|
|
3107
|
+
if (!match) {
|
|
3108
|
+
return err(new ValidationError(`Team not found: ${teamName}`));
|
|
3109
|
+
}
|
|
3110
|
+
return ok(String(match.id));
|
|
3111
|
+
}
|
|
3112
|
+
async function fetchFromAflApi(query) {
|
|
3113
|
+
const client = new AflApiClient();
|
|
3114
|
+
const competition = query.competition ?? "AFLM";
|
|
3115
|
+
const season = query.season ?? (/* @__PURE__ */ new Date()).getFullYear();
|
|
3116
|
+
const [teamIdResult, seasonResult] = await Promise.all([
|
|
3117
|
+
resolveTeamId(client, query.team, competition),
|
|
3118
|
+
client.resolveCompSeason(competition, season)
|
|
3119
|
+
]);
|
|
3120
|
+
if (!teamIdResult.success) return teamIdResult;
|
|
3121
|
+
if (!seasonResult.success) return seasonResult;
|
|
3122
|
+
const teamId = Number.parseInt(teamIdResult.data, 10);
|
|
3123
|
+
if (Number.isNaN(teamId)) {
|
|
3124
|
+
return err(new ValidationError(`Invalid team ID: ${teamIdResult.data}`));
|
|
3125
|
+
}
|
|
3126
|
+
const squadResult = await client.fetchSquad(teamId, seasonResult.data);
|
|
3127
|
+
if (!squadResult.success) return squadResult;
|
|
3128
|
+
const teamName = normaliseTeamName(squadResult.data.squad.team?.name ?? query.team);
|
|
3129
|
+
const players = squadResult.data.squad.players.map((p) => ({
|
|
3130
|
+
playerId: p.player.providerId ?? String(p.player.id),
|
|
3131
|
+
givenName: p.player.firstName,
|
|
3132
|
+
surname: p.player.surname,
|
|
3133
|
+
displayName: `${p.player.firstName} ${p.player.surname}`,
|
|
3134
|
+
team: teamName,
|
|
3135
|
+
jumperNumber: p.jumperNumber ?? null,
|
|
3136
|
+
position: p.position ?? null,
|
|
3137
|
+
dateOfBirth: p.player.dateOfBirth ?? null,
|
|
3138
|
+
heightCm: p.player.heightInCm ?? null,
|
|
3139
|
+
weightKg: p.player.weightInKg ?? null,
|
|
3140
|
+
gamesPlayed: null,
|
|
3141
|
+
goals: null,
|
|
3142
|
+
draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
|
|
3143
|
+
draftPosition: p.player.draftPosition ? Number.parseInt(p.player.draftPosition, 10) || null : null,
|
|
3144
|
+
draftType: p.player.draftType ?? null,
|
|
3145
|
+
debutYear: p.player.debutYear ? Number.parseInt(p.player.debutYear, 10) || null : null,
|
|
3146
|
+
recruitedFrom: p.player.recruitedFrom ?? null,
|
|
3147
|
+
source: "afl-api",
|
|
3148
|
+
competition
|
|
3149
|
+
}));
|
|
3150
|
+
return ok(players);
|
|
3151
|
+
}
|
|
3152
|
+
async function fetchFromFootyWire(query) {
|
|
3153
|
+
const client = new FootyWireClient();
|
|
3154
|
+
const competition = query.competition ?? "AFLM";
|
|
3155
|
+
const teamName = normaliseTeamName(query.team);
|
|
3156
|
+
const result = await client.fetchPlayerList(teamName);
|
|
3157
|
+
if (!result.success) return result;
|
|
3158
|
+
const players = result.data.map((p) => ({
|
|
3159
|
+
...p,
|
|
3160
|
+
source: "footywire",
|
|
3161
|
+
competition
|
|
3162
|
+
}));
|
|
3163
|
+
return ok(players);
|
|
3164
|
+
}
|
|
3165
|
+
async function fetchFromAflTables(query) {
|
|
3166
|
+
const client = new AflTablesClient();
|
|
3167
|
+
const competition = query.competition ?? "AFLM";
|
|
3168
|
+
const teamName = normaliseTeamName(query.team);
|
|
3169
|
+
const result = await client.fetchPlayerList(teamName);
|
|
3170
|
+
if (!result.success) return result;
|
|
3171
|
+
const players = result.data.map((p) => ({
|
|
3172
|
+
...p,
|
|
3173
|
+
source: "afl-tables",
|
|
3174
|
+
competition
|
|
3175
|
+
}));
|
|
3176
|
+
return ok(players);
|
|
3177
|
+
}
|
|
3178
|
+
async function fetchPlayerDetails(query) {
|
|
3179
|
+
switch (query.source) {
|
|
3180
|
+
case "afl-api":
|
|
3181
|
+
return fetchFromAflApi(query);
|
|
3182
|
+
case "footywire":
|
|
3183
|
+
return fetchFromFootyWire(query);
|
|
3184
|
+
case "afl-tables":
|
|
3185
|
+
return fetchFromAflTables(query);
|
|
3186
|
+
default:
|
|
3187
|
+
return err(
|
|
3188
|
+
new UnsupportedSourceError(
|
|
3189
|
+
`Source "${query.source}" is not supported for player details. Use "afl-api", "footywire", or "afl-tables".`,
|
|
3190
|
+
query.source
|
|
3191
|
+
)
|
|
3192
|
+
);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
var init_player_details = __esm({
|
|
3196
|
+
"src/api/player-details.ts"() {
|
|
3197
|
+
"use strict";
|
|
3198
|
+
init_errors();
|
|
3199
|
+
init_result();
|
|
3200
|
+
init_team_mapping();
|
|
3201
|
+
init_afl_api();
|
|
3202
|
+
init_afl_tables();
|
|
3203
|
+
init_footywire();
|
|
3204
|
+
}
|
|
3205
|
+
});
|
|
3206
|
+
|
|
3207
|
+
// src/transforms/player-stats.ts
|
|
3208
|
+
function toNullable(value) {
|
|
3209
|
+
return value ?? null;
|
|
3210
|
+
}
|
|
3211
|
+
function transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap) {
|
|
3212
|
+
const inner = item.player.player.player;
|
|
3213
|
+
const stats = item.playerStats.stats;
|
|
3214
|
+
const clearances = stats.clearances;
|
|
3215
|
+
return {
|
|
3216
|
+
matchId,
|
|
3217
|
+
season,
|
|
3218
|
+
roundNumber,
|
|
3219
|
+
team: normaliseTeamName(teamIdMap?.get(item.teamId) ?? item.teamId),
|
|
3220
|
+
competition,
|
|
3221
|
+
playerId: inner.playerId,
|
|
3222
|
+
givenName: inner.playerName.givenName,
|
|
3223
|
+
surname: inner.playerName.surname,
|
|
3224
|
+
displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
|
|
3225
|
+
jumperNumber: item.player.jumperNumber ?? null,
|
|
3226
|
+
kicks: toNullable(stats.kicks),
|
|
3227
|
+
handballs: toNullable(stats.handballs),
|
|
3228
|
+
disposals: toNullable(stats.disposals),
|
|
3229
|
+
marks: toNullable(stats.marks),
|
|
3230
|
+
goals: toNullable(stats.goals),
|
|
3231
|
+
behinds: toNullable(stats.behinds),
|
|
3232
|
+
tackles: toNullable(stats.tackles),
|
|
3233
|
+
hitouts: toNullable(stats.hitouts),
|
|
3234
|
+
freesFor: toNullable(stats.freesFor),
|
|
3235
|
+
freesAgainst: toNullable(stats.freesAgainst),
|
|
3236
|
+
contestedPossessions: toNullable(stats.contestedPossessions),
|
|
3237
|
+
uncontestedPossessions: toNullable(stats.uncontestedPossessions),
|
|
3238
|
+
contestedMarks: toNullable(stats.contestedMarks),
|
|
3239
|
+
intercepts: toNullable(stats.intercepts),
|
|
3240
|
+
centreClearances: toNullable(clearances?.centreClearances),
|
|
3241
|
+
stoppageClearances: toNullable(clearances?.stoppageClearances),
|
|
3242
|
+
totalClearances: toNullable(clearances?.totalClearances),
|
|
3243
|
+
inside50s: toNullable(stats.inside50s),
|
|
3244
|
+
rebound50s: toNullable(stats.rebound50s),
|
|
3245
|
+
clangers: toNullable(stats.clangers),
|
|
3246
|
+
turnovers: toNullable(stats.turnovers),
|
|
3247
|
+
onePercenters: toNullable(stats.onePercenters),
|
|
3248
|
+
bounces: toNullable(stats.bounces),
|
|
3249
|
+
goalAssists: toNullable(stats.goalAssists),
|
|
3250
|
+
disposalEfficiency: toNullable(stats.disposalEfficiency),
|
|
3251
|
+
metresGained: toNullable(stats.metresGained),
|
|
3252
|
+
goalAccuracy: toNullable(stats.goalAccuracy),
|
|
3253
|
+
marksInside50: toNullable(stats.marksInside50),
|
|
3254
|
+
tacklesInside50: toNullable(stats.tacklesInside50),
|
|
3255
|
+
shotsAtGoal: toNullable(stats.shotsAtGoal),
|
|
3256
|
+
scoreInvolvements: toNullable(stats.scoreInvolvements),
|
|
3257
|
+
totalPossessions: toNullable(stats.totalPossessions),
|
|
3258
|
+
timeOnGroundPercentage: toNullable(item.playerStats.timeOnGroundPercentage),
|
|
3259
|
+
ratingPoints: toNullable(stats.ratingPoints),
|
|
3260
|
+
dreamTeamPoints: toNullable(stats.dreamTeamPoints),
|
|
3261
|
+
effectiveDisposals: toNullable(stats.extendedStats?.effectiveDisposals),
|
|
3262
|
+
effectiveKicks: toNullable(stats.extendedStats?.effectiveKicks),
|
|
3263
|
+
kickEfficiency: toNullable(stats.extendedStats?.kickEfficiency),
|
|
3264
|
+
kickToHandballRatio: toNullable(stats.extendedStats?.kickToHandballRatio),
|
|
3265
|
+
pressureActs: toNullable(stats.extendedStats?.pressureActs),
|
|
3266
|
+
defHalfPressureActs: toNullable(stats.extendedStats?.defHalfPressureActs),
|
|
3267
|
+
spoils: toNullable(stats.extendedStats?.spoils),
|
|
3268
|
+
hitoutsToAdvantage: toNullable(stats.extendedStats?.hitoutsToAdvantage),
|
|
3269
|
+
hitoutWinPercentage: toNullable(stats.extendedStats?.hitoutWinPercentage),
|
|
3270
|
+
hitoutToAdvantageRate: toNullable(stats.extendedStats?.hitoutToAdvantageRate),
|
|
3271
|
+
groundBallGets: toNullable(stats.extendedStats?.groundBallGets),
|
|
3272
|
+
f50GroundBallGets: toNullable(stats.extendedStats?.f50GroundBallGets),
|
|
3273
|
+
interceptMarks: toNullable(stats.extendedStats?.interceptMarks),
|
|
3274
|
+
marksOnLead: toNullable(stats.extendedStats?.marksOnLead),
|
|
3275
|
+
contestedPossessionRate: toNullable(stats.extendedStats?.contestedPossessionRate),
|
|
3276
|
+
contestOffOneOnOnes: toNullable(stats.extendedStats?.contestOffOneOnOnes),
|
|
3277
|
+
contestOffWins: toNullable(stats.extendedStats?.contestOffWins),
|
|
3278
|
+
contestOffWinsPercentage: toNullable(stats.extendedStats?.contestOffWinsPercentage),
|
|
3279
|
+
contestDefOneOnOnes: toNullable(stats.extendedStats?.contestDefOneOnOnes),
|
|
3280
|
+
contestDefLosses: toNullable(stats.extendedStats?.contestDefLosses),
|
|
3281
|
+
contestDefLossPercentage: toNullable(stats.extendedStats?.contestDefLossPercentage),
|
|
3282
|
+
centreBounceAttendances: toNullable(stats.extendedStats?.centreBounceAttendances),
|
|
3283
|
+
kickins: toNullable(stats.extendedStats?.kickins),
|
|
3284
|
+
kickinsPlayon: toNullable(stats.extendedStats?.kickinsPlayon),
|
|
3285
|
+
ruckContests: toNullable(stats.extendedStats?.ruckContests),
|
|
3286
|
+
scoreLaunches: toNullable(stats.extendedStats?.scoreLaunches),
|
|
3287
|
+
source
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
function transformPlayerStats(data, matchId, season, roundNumber, competition, source = "afl-api", teamIdMap) {
|
|
3291
|
+
const home = data.homeTeamPlayerStats.map(
|
|
3292
|
+
(item) => transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap)
|
|
3293
|
+
);
|
|
3294
|
+
const away = data.awayTeamPlayerStats.map(
|
|
3295
|
+
(item) => transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap)
|
|
3296
|
+
);
|
|
3297
|
+
return [...home, ...away];
|
|
3298
|
+
}
|
|
3299
|
+
var init_player_stats = __esm({
|
|
3300
|
+
"src/transforms/player-stats.ts"() {
|
|
3301
|
+
"use strict";
|
|
3302
|
+
init_team_mapping();
|
|
3303
|
+
}
|
|
3304
|
+
});
|
|
3305
|
+
|
|
3306
|
+
// src/api/player-stats.ts
|
|
3307
|
+
async function fetchPlayerStats(query) {
|
|
3308
|
+
const competition = query.competition ?? "AFLM";
|
|
3309
|
+
switch (query.source) {
|
|
3310
|
+
case "afl-api": {
|
|
3311
|
+
const client = new AflApiClient();
|
|
3312
|
+
if (query.matchId) {
|
|
3313
|
+
const result = await client.fetchPlayerStats(query.matchId);
|
|
3314
|
+
if (!result.success) return result;
|
|
3315
|
+
return ok(
|
|
3316
|
+
transformPlayerStats(
|
|
3317
|
+
result.data,
|
|
3318
|
+
query.matchId,
|
|
3319
|
+
query.season,
|
|
3320
|
+
query.round ?? 0,
|
|
3321
|
+
competition
|
|
3322
|
+
)
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
3326
|
+
if (!seasonResult.success) return seasonResult;
|
|
3327
|
+
const roundNumber = query.round ?? 1;
|
|
3328
|
+
const matchItemsResult = await client.fetchRoundMatchItemsByNumber(
|
|
3329
|
+
seasonResult.data,
|
|
3330
|
+
roundNumber
|
|
3331
|
+
);
|
|
3332
|
+
if (!matchItemsResult.success) return matchItemsResult;
|
|
3333
|
+
const teamIdMap = /* @__PURE__ */ new Map();
|
|
3334
|
+
for (const item of matchItemsResult.data) {
|
|
3335
|
+
teamIdMap.set(item.match.homeTeamId, item.match.homeTeam.name);
|
|
3336
|
+
teamIdMap.set(item.match.awayTeamId, item.match.awayTeam.name);
|
|
3337
|
+
}
|
|
3338
|
+
const statsResults = await Promise.all(
|
|
3339
|
+
matchItemsResult.data.map((item) => client.fetchPlayerStats(item.match.matchId))
|
|
3340
|
+
);
|
|
3341
|
+
const allStats = [];
|
|
3342
|
+
for (let i = 0; i < statsResults.length; i++) {
|
|
3343
|
+
const statsResult = statsResults[i];
|
|
3344
|
+
if (!statsResult?.success)
|
|
3345
|
+
return statsResult ?? err(new AflApiError("Missing stats result"));
|
|
3346
|
+
const item = matchItemsResult.data[i];
|
|
3347
|
+
if (!item) continue;
|
|
3348
|
+
allStats.push(
|
|
3349
|
+
...transformPlayerStats(
|
|
3350
|
+
statsResult.data,
|
|
3351
|
+
item.match.matchId,
|
|
3352
|
+
query.season,
|
|
3353
|
+
roundNumber,
|
|
3354
|
+
competition,
|
|
3355
|
+
"afl-api",
|
|
3356
|
+
teamIdMap
|
|
3357
|
+
)
|
|
3358
|
+
);
|
|
3359
|
+
}
|
|
3360
|
+
return ok(allStats);
|
|
3361
|
+
}
|
|
3362
|
+
case "footywire": {
|
|
3363
|
+
const fwClient = new FootyWireClient();
|
|
3364
|
+
const idsResult = await fwClient.fetchSeasonMatchIds(query.season);
|
|
3365
|
+
if (!idsResult.success) return idsResult;
|
|
3366
|
+
const matchIds = idsResult.data;
|
|
3367
|
+
if (matchIds.length === 0) {
|
|
3368
|
+
return ok([]);
|
|
3369
|
+
}
|
|
3370
|
+
const allStats = [];
|
|
3371
|
+
const batchSize = 5;
|
|
3372
|
+
for (let i = 0; i < matchIds.length; i += batchSize) {
|
|
3373
|
+
const batch = matchIds.slice(i, i + batchSize);
|
|
3374
|
+
const results = await Promise.all(
|
|
3375
|
+
batch.map((mid) => fwClient.fetchMatchPlayerStats(mid, query.season, query.round ?? 0))
|
|
3376
|
+
);
|
|
3377
|
+
for (const result of results) {
|
|
3378
|
+
if (result.success) {
|
|
3379
|
+
allStats.push(...result.data);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
if (i + batchSize < matchIds.length) {
|
|
3383
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
if (query.round != null) {
|
|
3387
|
+
return ok(allStats.filter((s) => s.roundNumber === query.round));
|
|
3388
|
+
}
|
|
3389
|
+
return ok(allStats);
|
|
3390
|
+
}
|
|
3391
|
+
case "afl-tables": {
|
|
3392
|
+
const atClient = new AflTablesClient();
|
|
3393
|
+
const atResult = await atClient.fetchSeasonPlayerStats(query.season);
|
|
3394
|
+
if (!atResult.success) return atResult;
|
|
3395
|
+
if (query.round != null) {
|
|
3396
|
+
return ok(atResult.data.filter((s) => s.roundNumber === query.round));
|
|
3397
|
+
}
|
|
3398
|
+
return atResult;
|
|
3399
|
+
}
|
|
3400
|
+
default:
|
|
3401
|
+
return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
var init_player_stats2 = __esm({
|
|
3405
|
+
"src/api/player-stats.ts"() {
|
|
3406
|
+
"use strict";
|
|
3407
|
+
init_errors();
|
|
3408
|
+
init_result();
|
|
3409
|
+
init_afl_api();
|
|
3410
|
+
init_afl_tables();
|
|
3411
|
+
init_footywire();
|
|
3412
|
+
init_player_stats();
|
|
3413
|
+
}
|
|
3414
|
+
});
|
|
3415
|
+
|
|
3416
|
+
// src/api/team-stats.ts
|
|
3417
|
+
async function fetchTeamStats(query) {
|
|
3418
|
+
const summaryType = query.summaryType ?? "totals";
|
|
3419
|
+
switch (query.source) {
|
|
3420
|
+
case "footywire": {
|
|
3421
|
+
const client = new FootyWireClient();
|
|
3422
|
+
return client.fetchTeamStats(query.season, summaryType);
|
|
3423
|
+
}
|
|
3424
|
+
case "afl-tables": {
|
|
3425
|
+
const client = new AflTablesClient();
|
|
3426
|
+
return client.fetchTeamStats(query.season);
|
|
3427
|
+
}
|
|
3428
|
+
case "afl-api":
|
|
3429
|
+
case "squiggle":
|
|
3430
|
+
return err(
|
|
3431
|
+
new UnsupportedSourceError(
|
|
3432
|
+
`Team stats are not available from ${query.source}. Use "footywire" or "afl-tables".`,
|
|
3433
|
+
query.source
|
|
3434
|
+
)
|
|
3435
|
+
);
|
|
3436
|
+
default:
|
|
3437
|
+
return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
var init_team_stats = __esm({
|
|
3441
|
+
"src/api/team-stats.ts"() {
|
|
3442
|
+
"use strict";
|
|
3443
|
+
init_errors();
|
|
3444
|
+
init_result();
|
|
3445
|
+
init_afl_tables();
|
|
3446
|
+
init_footywire();
|
|
3447
|
+
}
|
|
3448
|
+
});
|
|
3449
|
+
|
|
3450
|
+
// src/api/teams.ts
|
|
3451
|
+
function teamTypeForComp(comp) {
|
|
3452
|
+
return comp === "AFLW" ? "WOMEN" : "MEN";
|
|
3453
|
+
}
|
|
3454
|
+
async function fetchTeams(query) {
|
|
3455
|
+
const client = new AflApiClient();
|
|
3456
|
+
const teamType = query?.teamType ?? teamTypeForComp(query?.competition ?? "AFLM");
|
|
3457
|
+
const result = await client.fetchTeams(teamType);
|
|
3458
|
+
if (!result.success) return result;
|
|
3459
|
+
const competition = query?.competition ?? "AFLM";
|
|
3460
|
+
const teams = result.data.map((t) => ({
|
|
3461
|
+
teamId: String(t.id),
|
|
3462
|
+
name: normaliseTeamName(t.name),
|
|
3463
|
+
abbreviation: t.abbreviation ?? "",
|
|
3464
|
+
competition
|
|
3465
|
+
})).filter((t) => AFL_SENIOR_TEAMS.has(t.name));
|
|
3466
|
+
return ok(teams);
|
|
3467
|
+
}
|
|
3468
|
+
async function fetchSquad(query) {
|
|
3469
|
+
const client = new AflApiClient();
|
|
3470
|
+
const competition = query.competition ?? "AFLM";
|
|
3471
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
3472
|
+
if (!seasonResult.success) return seasonResult;
|
|
3473
|
+
const teamId = Number.parseInt(query.teamId, 10);
|
|
3474
|
+
if (Number.isNaN(teamId)) {
|
|
3475
|
+
return err(new ValidationError(`Invalid team ID: ${query.teamId}`));
|
|
3476
|
+
}
|
|
3477
|
+
const squadResult = await client.fetchSquad(teamId, seasonResult.data);
|
|
3478
|
+
if (!squadResult.success) return squadResult;
|
|
3479
|
+
const players = squadResult.data.squad.players.map((p) => ({
|
|
3480
|
+
playerId: p.player.providerId ?? String(p.player.id),
|
|
3481
|
+
givenName: p.player.firstName,
|
|
3482
|
+
surname: p.player.surname,
|
|
3483
|
+
displayName: `${p.player.firstName} ${p.player.surname}`,
|
|
3484
|
+
jumperNumber: p.jumperNumber ?? null,
|
|
3485
|
+
position: p.position ?? null,
|
|
3486
|
+
dateOfBirth: p.player.dateOfBirth ? new Date(p.player.dateOfBirth) : null,
|
|
3487
|
+
heightCm: p.player.heightInCm ?? null,
|
|
3488
|
+
weightKg: p.player.weightInKg ?? null,
|
|
3489
|
+
draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
|
|
3490
|
+
draftPosition: p.player.draftPosition ? Number.parseInt(p.player.draftPosition, 10) || null : null,
|
|
3491
|
+
draftType: p.player.draftType ?? null,
|
|
3492
|
+
debutYear: p.player.debutYear ? Number.parseInt(p.player.debutYear, 10) || null : null,
|
|
3493
|
+
recruitedFrom: p.player.recruitedFrom ?? null
|
|
3494
|
+
}));
|
|
3495
|
+
return ok({
|
|
3496
|
+
teamId: query.teamId,
|
|
3497
|
+
teamName: normaliseTeamName(squadResult.data.squad.team?.name ?? query.teamId),
|
|
3498
|
+
season: query.season,
|
|
3499
|
+
players,
|
|
3500
|
+
competition
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
var init_teams = __esm({
|
|
3504
|
+
"src/api/teams.ts"() {
|
|
3505
|
+
"use strict";
|
|
3506
|
+
init_errors();
|
|
3507
|
+
init_result();
|
|
3508
|
+
init_team_mapping();
|
|
3509
|
+
init_afl_api();
|
|
3510
|
+
}
|
|
3511
|
+
});
|
|
18
3512
|
|
|
19
3513
|
// src/index.ts
|
|
20
|
-
var
|
|
3514
|
+
var init_index = __esm({
|
|
3515
|
+
"src/index.ts"() {
|
|
3516
|
+
"use strict";
|
|
3517
|
+
init_coaches_votes();
|
|
3518
|
+
init_fixture();
|
|
3519
|
+
init_ladder2();
|
|
3520
|
+
init_lineup2();
|
|
3521
|
+
init_match_results2();
|
|
3522
|
+
init_player_details();
|
|
3523
|
+
init_player_stats2();
|
|
3524
|
+
init_team_stats();
|
|
3525
|
+
init_teams();
|
|
3526
|
+
}
|
|
3527
|
+
});
|
|
21
3528
|
|
|
22
3529
|
// src/cli/formatters/csv.ts
|
|
23
3530
|
function escapeField(value) {
|
|
24
|
-
if (value.includes(",") || value.includes('"') || value.includes(
|
|
25
|
-
`) || value.includes("\r")) {
|
|
3531
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
|
|
26
3532
|
return `"${value.replace(/"/g, '""')}"`;
|
|
27
3533
|
}
|
|
28
3534
|
return value;
|
|
29
3535
|
}
|
|
30
3536
|
function toStringValue(value) {
|
|
31
|
-
if (value === null || value ===
|
|
32
|
-
|
|
33
|
-
if (value
|
|
34
|
-
return value.toISOString();
|
|
35
|
-
if (typeof value === "object")
|
|
36
|
-
return JSON.stringify(value);
|
|
3537
|
+
if (value === null || value === void 0) return "";
|
|
3538
|
+
if (value instanceof Date) return value.toISOString();
|
|
3539
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
37
3540
|
return String(value);
|
|
38
3541
|
}
|
|
39
3542
|
function formatCsv(data) {
|
|
40
|
-
if (data.length === 0)
|
|
41
|
-
return "";
|
|
3543
|
+
if (data.length === 0) return "";
|
|
42
3544
|
const firstRow = data[0];
|
|
43
|
-
if (!firstRow)
|
|
44
|
-
return "";
|
|
3545
|
+
if (!firstRow) return "";
|
|
45
3546
|
const headers = Object.keys(firstRow);
|
|
46
3547
|
const lines = [headers.map(escapeField).join(",")];
|
|
47
3548
|
for (const row of data) {
|
|
48
3549
|
const values = headers.map((h) => escapeField(toStringValue(row[h])));
|
|
49
3550
|
lines.push(values.join(","));
|
|
50
3551
|
}
|
|
51
|
-
return lines.join(
|
|
52
|
-
`);
|
|
3552
|
+
return lines.join("\n");
|
|
53
3553
|
}
|
|
3554
|
+
var init_csv = __esm({
|
|
3555
|
+
"src/cli/formatters/csv.ts"() {
|
|
3556
|
+
"use strict";
|
|
3557
|
+
}
|
|
3558
|
+
});
|
|
54
3559
|
|
|
55
3560
|
// src/cli/formatters/json.ts
|
|
56
3561
|
function formatJson(data) {
|
|
57
3562
|
return JSON.stringify(data, null, 2);
|
|
58
3563
|
}
|
|
3564
|
+
var init_json = __esm({
|
|
3565
|
+
"src/cli/formatters/json.ts"() {
|
|
3566
|
+
"use strict";
|
|
3567
|
+
}
|
|
3568
|
+
});
|
|
59
3569
|
|
|
60
3570
|
// src/cli/formatters/table.ts
|
|
61
3571
|
function toDisplayValue(value) {
|
|
62
|
-
if (value === null || value ===
|
|
63
|
-
|
|
64
|
-
if (value
|
|
65
|
-
return value.toISOString().slice(0, 16).replace("T", " ");
|
|
66
|
-
if (typeof value === "object")
|
|
67
|
-
return JSON.stringify(value);
|
|
3572
|
+
if (value === null || value === void 0) return "-";
|
|
3573
|
+
if (value instanceof Date) return value.toISOString().slice(0, 16).replace("T", " ");
|
|
3574
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
68
3575
|
return String(value);
|
|
69
3576
|
}
|
|
70
3577
|
function truncate(str, maxLen) {
|
|
71
|
-
if (str.length <= maxLen)
|
|
72
|
-
|
|
73
|
-
return `${str.slice(0, maxLen - 1)}…`;
|
|
3578
|
+
if (str.length <= maxLen) return str;
|
|
3579
|
+
return `${str.slice(0, maxLen - 1)}\u2026`;
|
|
74
3580
|
}
|
|
75
3581
|
function formatTable(data, options = {}) {
|
|
76
|
-
if (data.length === 0)
|
|
77
|
-
return "No data.";
|
|
3582
|
+
if (data.length === 0) return "No data.";
|
|
78
3583
|
const firstRow = data[0];
|
|
79
|
-
if (!firstRow)
|
|
80
|
-
return "No data.";
|
|
3584
|
+
if (!firstRow) return "No data.";
|
|
81
3585
|
const termWidth = options.terminalWidth ?? process.stdout.columns ?? 120;
|
|
82
3586
|
const allKeys = Object.keys(firstRow);
|
|
83
3587
|
let columns;
|
|
@@ -88,78 +3592,72 @@ function formatTable(data, options = {}) {
|
|
|
88
3592
|
}
|
|
89
3593
|
const colWidths = columns.map((col) => (col.label ?? col.key).length);
|
|
90
3594
|
for (const row of data) {
|
|
91
|
-
for (let i = 0;i < columns.length; i++) {
|
|
3595
|
+
for (let i = 0; i < columns.length; i++) {
|
|
92
3596
|
const col = columns[i];
|
|
93
|
-
if (!col)
|
|
94
|
-
continue;
|
|
3597
|
+
if (!col) continue;
|
|
95
3598
|
const len = toDisplayValue(row[col.key]).length;
|
|
96
3599
|
const current = colWidths[i];
|
|
97
|
-
if (current !==
|
|
3600
|
+
if (current !== void 0 && len > current) {
|
|
98
3601
|
colWidths[i] = len;
|
|
99
3602
|
}
|
|
100
3603
|
}
|
|
101
3604
|
}
|
|
102
|
-
for (let i = 0;i < columns.length; i++) {
|
|
3605
|
+
for (let i = 0; i < columns.length; i++) {
|
|
103
3606
|
const col = columns[i];
|
|
104
3607
|
const width = colWidths[i];
|
|
105
|
-
if (!col || width ===
|
|
106
|
-
continue;
|
|
3608
|
+
if (!col || width === void 0) continue;
|
|
107
3609
|
colWidths[i] = Math.min(col.maxWidth ?? 30, width);
|
|
108
3610
|
}
|
|
109
3611
|
const gap = 2;
|
|
110
3612
|
const visibleCols = [];
|
|
111
3613
|
let usedWidth = 0;
|
|
112
|
-
for (let i = 0;i < columns.length; i++) {
|
|
3614
|
+
for (let i = 0; i < columns.length; i++) {
|
|
113
3615
|
const colWidth = colWidths[i];
|
|
114
|
-
if (colWidth ===
|
|
115
|
-
continue;
|
|
3616
|
+
if (colWidth === void 0) continue;
|
|
116
3617
|
const needed = usedWidth > 0 ? colWidth + gap : colWidth;
|
|
117
|
-
if (usedWidth + needed > termWidth && visibleCols.length > 0)
|
|
118
|
-
break;
|
|
3618
|
+
if (usedWidth + needed > termWidth && visibleCols.length > 0) break;
|
|
119
3619
|
visibleCols.push(i);
|
|
120
3620
|
usedWidth += needed;
|
|
121
3621
|
}
|
|
122
3622
|
const headerParts = visibleCols.map((i) => {
|
|
123
3623
|
const col = columns[i];
|
|
124
3624
|
const width = colWidths[i];
|
|
125
|
-
if (!col || width ===
|
|
126
|
-
return "";
|
|
3625
|
+
if (!col || width === void 0) return "";
|
|
127
3626
|
const label = (col.label ?? col.key).toUpperCase();
|
|
128
3627
|
return truncate(label, width).padEnd(width);
|
|
129
3628
|
});
|
|
130
3629
|
const header = headerParts.join(" ");
|
|
131
3630
|
const separator = visibleCols.map((i) => {
|
|
132
3631
|
const width = colWidths[i];
|
|
133
|
-
if (width ===
|
|
134
|
-
|
|
135
|
-
return "─".repeat(width);
|
|
3632
|
+
if (width === void 0) return "";
|
|
3633
|
+
return "\u2500".repeat(width);
|
|
136
3634
|
}).join(" ");
|
|
137
3635
|
const rows = data.map((row) => {
|
|
138
3636
|
const parts = visibleCols.map((i) => {
|
|
139
3637
|
const col = columns[i];
|
|
140
3638
|
const width = colWidths[i];
|
|
141
|
-
if (!col || width ===
|
|
142
|
-
return "";
|
|
3639
|
+
if (!col || width === void 0) return "";
|
|
143
3640
|
const val = toDisplayValue(row[col.key]);
|
|
144
3641
|
return truncate(val, width).padEnd(width);
|
|
145
3642
|
});
|
|
146
3643
|
return parts.join(" ");
|
|
147
3644
|
});
|
|
148
|
-
return [header, separator, ...rows].join(
|
|
149
|
-
`);
|
|
3645
|
+
return [header, separator, ...rows].join("\n");
|
|
150
3646
|
}
|
|
3647
|
+
var init_table = __esm({
|
|
3648
|
+
"src/cli/formatters/table.ts"() {
|
|
3649
|
+
"use strict";
|
|
3650
|
+
}
|
|
3651
|
+
});
|
|
151
3652
|
|
|
152
3653
|
// src/cli/formatters/index.ts
|
|
153
3654
|
function resolveFormat(options) {
|
|
154
|
-
if (options.json)
|
|
155
|
-
|
|
156
|
-
if (options.csv)
|
|
157
|
-
return "csv";
|
|
3655
|
+
if (options.json) return "json";
|
|
3656
|
+
if (options.csv) return "csv";
|
|
158
3657
|
if (options.format === "json" || options.format === "csv" || options.format === "table") {
|
|
159
3658
|
return options.format;
|
|
160
3659
|
}
|
|
161
|
-
if (!process.stdout.isTTY)
|
|
162
|
-
return "json";
|
|
3660
|
+
if (!process.stdout.isTTY) return "json";
|
|
163
3661
|
return "table";
|
|
164
3662
|
}
|
|
165
3663
|
function formatOutput(data, options) {
|
|
@@ -177,7 +3675,14 @@ function formatOutput(data, options) {
|
|
|
177
3675
|
});
|
|
178
3676
|
}
|
|
179
3677
|
}
|
|
180
|
-
var init_formatters = (
|
|
3678
|
+
var init_formatters = __esm({
|
|
3679
|
+
"src/cli/formatters/index.ts"() {
|
|
3680
|
+
"use strict";
|
|
3681
|
+
init_csv();
|
|
3682
|
+
init_json();
|
|
3683
|
+
init_table();
|
|
3684
|
+
}
|
|
3685
|
+
});
|
|
181
3686
|
|
|
182
3687
|
// src/cli/ui.ts
|
|
183
3688
|
import { spinner } from "@clack/prompts";
|
|
@@ -198,517 +3703,747 @@ async function withSpinner(message, fn) {
|
|
|
198
3703
|
}
|
|
199
3704
|
}
|
|
200
3705
|
function showSummary(message) {
|
|
201
|
-
if (!isTTY)
|
|
202
|
-
return;
|
|
3706
|
+
if (!isTTY) return;
|
|
203
3707
|
console.error(pc.dim(message));
|
|
204
3708
|
}
|
|
205
3709
|
var isTTY;
|
|
206
|
-
var init_ui = __esm(
|
|
207
|
-
|
|
3710
|
+
var init_ui = __esm({
|
|
3711
|
+
"src/cli/ui.ts"() {
|
|
3712
|
+
"use strict";
|
|
3713
|
+
isTTY = process.stdout.isTTY === true;
|
|
3714
|
+
}
|
|
208
3715
|
});
|
|
209
3716
|
|
|
210
3717
|
// src/cli/commands/matches.ts
|
|
211
|
-
var
|
|
212
|
-
__export(
|
|
3718
|
+
var matches_exports = {};
|
|
3719
|
+
__export(matches_exports, {
|
|
213
3720
|
matchesCommand: () => matchesCommand
|
|
214
3721
|
});
|
|
215
3722
|
import { defineCommand } from "citty";
|
|
216
3723
|
var DEFAULT_COLUMNS, matchesCommand;
|
|
217
|
-
var init_matches = __esm(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
237
|
-
round: { type: "string", description: "Round number" },
|
|
238
|
-
source: { type: "string", description: "Data source", default: "afl-api" },
|
|
239
|
-
competition: {
|
|
240
|
-
type: "string",
|
|
241
|
-
description: "Competition code (AFLM or AFLW)",
|
|
242
|
-
default: "AFLM"
|
|
3724
|
+
var init_matches = __esm({
|
|
3725
|
+
"src/cli/commands/matches.ts"() {
|
|
3726
|
+
"use strict";
|
|
3727
|
+
init_index();
|
|
3728
|
+
init_formatters();
|
|
3729
|
+
init_ui();
|
|
3730
|
+
DEFAULT_COLUMNS = [
|
|
3731
|
+
{ key: "date", label: "Date", maxWidth: 16 },
|
|
3732
|
+
{ key: "roundNumber", label: "Round", maxWidth: 6 },
|
|
3733
|
+
{ key: "homeTeam", label: "Home", maxWidth: 20 },
|
|
3734
|
+
{ key: "awayTeam", label: "Away", maxWidth: 20 },
|
|
3735
|
+
{ key: "homePoints", label: "H.Pts", maxWidth: 6 },
|
|
3736
|
+
{ key: "awayPoints", label: "A.Pts", maxWidth: 6 },
|
|
3737
|
+
{ key: "venue", label: "Venue", maxWidth: 24 }
|
|
3738
|
+
];
|
|
3739
|
+
matchesCommand = defineCommand({
|
|
3740
|
+
meta: {
|
|
3741
|
+
name: "matches",
|
|
3742
|
+
description: "Fetch match results for a season"
|
|
243
3743
|
},
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
3744
|
+
args: {
|
|
3745
|
+
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
3746
|
+
round: { type: "string", description: "Round number" },
|
|
3747
|
+
source: { type: "string", description: "Data source", default: "afl-api" },
|
|
3748
|
+
competition: {
|
|
3749
|
+
type: "string",
|
|
3750
|
+
description: "Competition code (AFLM or AFLW)",
|
|
3751
|
+
default: "AFLM"
|
|
3752
|
+
},
|
|
3753
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
3754
|
+
csv: { type: "boolean", description: "Output as CSV" },
|
|
3755
|
+
format: { type: "string", description: "Output format: table, json, csv" },
|
|
3756
|
+
full: { type: "boolean", description: "Show all columns in table output" }
|
|
3757
|
+
},
|
|
3758
|
+
async run({ args }) {
|
|
3759
|
+
const season = Number(args.season);
|
|
3760
|
+
const round = args.round ? Number(args.round) : void 0;
|
|
3761
|
+
const result = await withSpinner(
|
|
3762
|
+
"Fetching match results\u2026",
|
|
3763
|
+
() => fetchMatchResults({
|
|
3764
|
+
source: args.source,
|
|
3765
|
+
season,
|
|
3766
|
+
round,
|
|
3767
|
+
competition: args.competition
|
|
3768
|
+
})
|
|
3769
|
+
);
|
|
3770
|
+
if (!result.success) {
|
|
3771
|
+
throw result.error;
|
|
3772
|
+
}
|
|
3773
|
+
const data = result.data;
|
|
3774
|
+
showSummary(`Loaded ${data.length} matches for ${season}${round ? ` round ${round}` : ""}`);
|
|
3775
|
+
const formatOptions = {
|
|
3776
|
+
json: args.json,
|
|
3777
|
+
csv: args.csv,
|
|
3778
|
+
format: args.format,
|
|
3779
|
+
full: args.full,
|
|
3780
|
+
columns: DEFAULT_COLUMNS
|
|
3781
|
+
};
|
|
3782
|
+
console.log(formatOutput(data, formatOptions));
|
|
3783
|
+
}
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
273
3786
|
});
|
|
274
3787
|
|
|
275
3788
|
// src/cli/commands/stats.ts
|
|
276
|
-
var
|
|
277
|
-
__export(
|
|
3789
|
+
var stats_exports = {};
|
|
3790
|
+
__export(stats_exports, {
|
|
278
3791
|
statsCommand: () => statsCommand
|
|
279
3792
|
});
|
|
280
3793
|
import { defineCommand as defineCommand2 } from "citty";
|
|
281
3794
|
var DEFAULT_COLUMNS2, statsCommand;
|
|
282
|
-
var init_stats = __esm(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
302
|
-
round: { type: "string", description: "Round number" },
|
|
303
|
-
"match-id": { type: "string", description: "Specific match ID" },
|
|
304
|
-
source: { type: "string", description: "Data source", default: "afl-api" },
|
|
305
|
-
competition: {
|
|
306
|
-
type: "string",
|
|
307
|
-
description: "Competition code (AFLM or AFLW)",
|
|
308
|
-
default: "AFLM"
|
|
3795
|
+
var init_stats = __esm({
|
|
3796
|
+
"src/cli/commands/stats.ts"() {
|
|
3797
|
+
"use strict";
|
|
3798
|
+
init_index();
|
|
3799
|
+
init_formatters();
|
|
3800
|
+
init_ui();
|
|
3801
|
+
DEFAULT_COLUMNS2 = [
|
|
3802
|
+
{ key: "displayName", label: "Player", maxWidth: 22 },
|
|
3803
|
+
{ key: "team", label: "Team", maxWidth: 18 },
|
|
3804
|
+
{ key: "disposals", label: "Disp", maxWidth: 6 },
|
|
3805
|
+
{ key: "kicks", label: "Kicks", maxWidth: 6 },
|
|
3806
|
+
{ key: "handballs", label: "HB", maxWidth: 6 },
|
|
3807
|
+
{ key: "marks", label: "Marks", maxWidth: 6 },
|
|
3808
|
+
{ key: "goals", label: "Goals", maxWidth: 6 }
|
|
3809
|
+
];
|
|
3810
|
+
statsCommand = defineCommand2({
|
|
3811
|
+
meta: {
|
|
3812
|
+
name: "stats",
|
|
3813
|
+
description: "Fetch player statistics for a season"
|
|
309
3814
|
},
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
3815
|
+
args: {
|
|
3816
|
+
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
3817
|
+
round: { type: "string", description: "Round number" },
|
|
3818
|
+
"match-id": { type: "string", description: "Specific match ID" },
|
|
3819
|
+
source: { type: "string", description: "Data source", default: "afl-api" },
|
|
3820
|
+
competition: {
|
|
3821
|
+
type: "string",
|
|
3822
|
+
description: "Competition code (AFLM or AFLW)",
|
|
3823
|
+
default: "AFLM"
|
|
3824
|
+
},
|
|
3825
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
3826
|
+
csv: { type: "boolean", description: "Output as CSV" },
|
|
3827
|
+
format: { type: "string", description: "Output format: table, json, csv" },
|
|
3828
|
+
full: { type: "boolean", description: "Show all columns in table output" }
|
|
3829
|
+
},
|
|
3830
|
+
async run({ args }) {
|
|
3831
|
+
const season = Number(args.season);
|
|
3832
|
+
const round = args.round ? Number(args.round) : void 0;
|
|
3833
|
+
const matchId = args["match-id"];
|
|
3834
|
+
const result = await withSpinner(
|
|
3835
|
+
"Fetching player stats\u2026",
|
|
3836
|
+
() => fetchPlayerStats({
|
|
3837
|
+
source: args.source,
|
|
3838
|
+
season,
|
|
3839
|
+
round,
|
|
3840
|
+
matchId,
|
|
3841
|
+
competition: args.competition
|
|
3842
|
+
})
|
|
3843
|
+
);
|
|
3844
|
+
if (!result.success) {
|
|
3845
|
+
throw result.error;
|
|
3846
|
+
}
|
|
3847
|
+
const data = result.data;
|
|
3848
|
+
showSummary(
|
|
3849
|
+
`Loaded ${data.length} player stat lines for ${season}${round ? ` round ${round}` : ""}`
|
|
3850
|
+
);
|
|
3851
|
+
const formatOptions = {
|
|
3852
|
+
json: args.json,
|
|
3853
|
+
csv: args.csv,
|
|
3854
|
+
format: args.format,
|
|
3855
|
+
full: args.full,
|
|
3856
|
+
columns: DEFAULT_COLUMNS2
|
|
3857
|
+
};
|
|
3858
|
+
console.log(formatOutput(data, formatOptions));
|
|
3859
|
+
}
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
341
3862
|
});
|
|
342
3863
|
|
|
343
3864
|
// src/cli/commands/fixture.ts
|
|
344
|
-
var
|
|
345
|
-
__export(
|
|
3865
|
+
var fixture_exports = {};
|
|
3866
|
+
__export(fixture_exports, {
|
|
346
3867
|
fixtureCommand: () => fixtureCommand
|
|
347
3868
|
});
|
|
348
3869
|
import { defineCommand as defineCommand3 } from "citty";
|
|
349
3870
|
var DEFAULT_COLUMNS3, fixtureCommand;
|
|
350
|
-
var
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
368
|
-
round: { type: "string", description: "Round number" },
|
|
369
|
-
source: { type: "string", description: "Data source", default: "afl-api" },
|
|
370
|
-
competition: {
|
|
371
|
-
type: "string",
|
|
372
|
-
description: "Competition code (AFLM or AFLW)",
|
|
373
|
-
default: "AFLM"
|
|
3871
|
+
var init_fixture2 = __esm({
|
|
3872
|
+
"src/cli/commands/fixture.ts"() {
|
|
3873
|
+
"use strict";
|
|
3874
|
+
init_index();
|
|
3875
|
+
init_formatters();
|
|
3876
|
+
init_ui();
|
|
3877
|
+
DEFAULT_COLUMNS3 = [
|
|
3878
|
+
{ key: "roundNumber", label: "Round", maxWidth: 6 },
|
|
3879
|
+
{ key: "date", label: "Date", maxWidth: 16 },
|
|
3880
|
+
{ key: "homeTeam", label: "Home", maxWidth: 20 },
|
|
3881
|
+
{ key: "awayTeam", label: "Away", maxWidth: 20 },
|
|
3882
|
+
{ key: "venue", label: "Venue", maxWidth: 24 }
|
|
3883
|
+
];
|
|
3884
|
+
fixtureCommand = defineCommand3({
|
|
3885
|
+
meta: {
|
|
3886
|
+
name: "fixture",
|
|
3887
|
+
description: "Fetch fixture/schedule for a season"
|
|
374
3888
|
},
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
3889
|
+
args: {
|
|
3890
|
+
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
3891
|
+
round: { type: "string", description: "Round number" },
|
|
3892
|
+
source: { type: "string", description: "Data source", default: "afl-api" },
|
|
3893
|
+
competition: {
|
|
3894
|
+
type: "string",
|
|
3895
|
+
description: "Competition code (AFLM or AFLW)",
|
|
3896
|
+
default: "AFLM"
|
|
3897
|
+
},
|
|
3898
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
3899
|
+
csv: { type: "boolean", description: "Output as CSV" },
|
|
3900
|
+
format: { type: "string", description: "Output format: table, json, csv" },
|
|
3901
|
+
full: { type: "boolean", description: "Show all columns in table output" }
|
|
3902
|
+
},
|
|
3903
|
+
async run({ args }) {
|
|
3904
|
+
const season = Number(args.season);
|
|
3905
|
+
const round = args.round ? Number(args.round) : void 0;
|
|
3906
|
+
const result = await withSpinner(
|
|
3907
|
+
"Fetching fixture\u2026",
|
|
3908
|
+
() => fetchFixture({
|
|
3909
|
+
source: args.source,
|
|
3910
|
+
season,
|
|
3911
|
+
round,
|
|
3912
|
+
competition: args.competition
|
|
3913
|
+
})
|
|
3914
|
+
);
|
|
3915
|
+
if (!result.success) {
|
|
3916
|
+
throw result.error;
|
|
3917
|
+
}
|
|
3918
|
+
const data = result.data;
|
|
3919
|
+
showSummary(`Loaded ${data.length} fixtures for ${season}${round ? ` round ${round}` : ""}`);
|
|
3920
|
+
const formatOptions = {
|
|
3921
|
+
json: args.json,
|
|
3922
|
+
csv: args.csv,
|
|
3923
|
+
format: args.format,
|
|
3924
|
+
full: args.full,
|
|
3925
|
+
columns: DEFAULT_COLUMNS3
|
|
3926
|
+
};
|
|
3927
|
+
console.log(formatOutput(data, formatOptions));
|
|
3928
|
+
}
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
404
3931
|
});
|
|
405
3932
|
|
|
406
3933
|
// src/cli/commands/ladder.ts
|
|
407
|
-
var
|
|
408
|
-
__export(
|
|
3934
|
+
var ladder_exports = {};
|
|
3935
|
+
__export(ladder_exports, {
|
|
409
3936
|
ladderCommand: () => ladderCommand
|
|
410
3937
|
});
|
|
411
3938
|
import { defineCommand as defineCommand4 } from "citty";
|
|
412
3939
|
var DEFAULT_COLUMNS4, ladderCommand;
|
|
413
|
-
var
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
433
|
-
round: { type: "string", description: "Round number" },
|
|
434
|
-
source: { type: "string", description: "Data source", default: "afl-api" },
|
|
435
|
-
competition: {
|
|
436
|
-
type: "string",
|
|
437
|
-
description: "Competition code (AFLM or AFLW)",
|
|
438
|
-
default: "AFLM"
|
|
3940
|
+
var init_ladder3 = __esm({
|
|
3941
|
+
"src/cli/commands/ladder.ts"() {
|
|
3942
|
+
"use strict";
|
|
3943
|
+
init_index();
|
|
3944
|
+
init_formatters();
|
|
3945
|
+
init_ui();
|
|
3946
|
+
DEFAULT_COLUMNS4 = [
|
|
3947
|
+
{ key: "position", label: "Pos", maxWidth: 4 },
|
|
3948
|
+
{ key: "team", label: "Team", maxWidth: 24 },
|
|
3949
|
+
{ key: "wins", label: "W", maxWidth: 4 },
|
|
3950
|
+
{ key: "losses", label: "L", maxWidth: 4 },
|
|
3951
|
+
{ key: "draws", label: "D", maxWidth: 4 },
|
|
3952
|
+
{ key: "percentage", label: "Pct", maxWidth: 8 },
|
|
3953
|
+
{ key: "premiershipsPoints", label: "Pts", maxWidth: 5 }
|
|
3954
|
+
];
|
|
3955
|
+
ladderCommand = defineCommand4({
|
|
3956
|
+
meta: {
|
|
3957
|
+
name: "ladder",
|
|
3958
|
+
description: "Fetch ladder standings for a season"
|
|
439
3959
|
},
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
3960
|
+
args: {
|
|
3961
|
+
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
3962
|
+
round: { type: "string", description: "Round number" },
|
|
3963
|
+
source: { type: "string", description: "Data source", default: "afl-api" },
|
|
3964
|
+
competition: {
|
|
3965
|
+
type: "string",
|
|
3966
|
+
description: "Competition code (AFLM or AFLW)",
|
|
3967
|
+
default: "AFLM"
|
|
3968
|
+
},
|
|
3969
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
3970
|
+
csv: { type: "boolean", description: "Output as CSV" },
|
|
3971
|
+
format: { type: "string", description: "Output format: table, json, csv" },
|
|
3972
|
+
full: { type: "boolean", description: "Show all columns in table output" }
|
|
3973
|
+
},
|
|
3974
|
+
async run({ args }) {
|
|
3975
|
+
const season = Number(args.season);
|
|
3976
|
+
const round = args.round ? Number(args.round) : void 0;
|
|
3977
|
+
const result = await withSpinner(
|
|
3978
|
+
"Fetching ladder\u2026",
|
|
3979
|
+
() => fetchLadder({
|
|
3980
|
+
source: args.source,
|
|
3981
|
+
season,
|
|
3982
|
+
round,
|
|
3983
|
+
competition: args.competition
|
|
3984
|
+
})
|
|
3985
|
+
);
|
|
3986
|
+
if (!result.success) {
|
|
3987
|
+
throw result.error;
|
|
3988
|
+
}
|
|
3989
|
+
const data = result.data;
|
|
3990
|
+
showSummary(
|
|
3991
|
+
`Loaded ladder for ${season}${round ? ` round ${round}` : ""} (${data.entries.length} teams)`
|
|
3992
|
+
);
|
|
3993
|
+
const formatOptions = {
|
|
3994
|
+
json: args.json,
|
|
3995
|
+
csv: args.csv,
|
|
3996
|
+
format: args.format,
|
|
3997
|
+
full: args.full,
|
|
3998
|
+
columns: DEFAULT_COLUMNS4
|
|
3999
|
+
};
|
|
4000
|
+
console.log(formatOutput(data.entries, formatOptions));
|
|
4001
|
+
}
|
|
4002
|
+
});
|
|
4003
|
+
}
|
|
469
4004
|
});
|
|
470
4005
|
|
|
471
4006
|
// src/cli/commands/lineup.ts
|
|
472
|
-
var
|
|
473
|
-
__export(
|
|
4007
|
+
var lineup_exports = {};
|
|
4008
|
+
__export(lineup_exports, {
|
|
474
4009
|
lineupCommand: () => lineupCommand
|
|
475
4010
|
});
|
|
476
4011
|
import { defineCommand as defineCommand5 } from "citty";
|
|
477
4012
|
var DEFAULT_COLUMNS5, lineupCommand;
|
|
478
|
-
var
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
494
|
-
round: { type: "string", description: "Round number", required: true },
|
|
495
|
-
"match-id": { type: "string", description: "Specific match ID" },
|
|
496
|
-
source: { type: "string", description: "Data source", default: "afl-api" },
|
|
497
|
-
competition: {
|
|
498
|
-
type: "string",
|
|
499
|
-
description: "Competition code (AFLM or AFLW)",
|
|
500
|
-
default: "AFLM"
|
|
4013
|
+
var init_lineup3 = __esm({
|
|
4014
|
+
"src/cli/commands/lineup.ts"() {
|
|
4015
|
+
"use strict";
|
|
4016
|
+
init_index();
|
|
4017
|
+
init_formatters();
|
|
4018
|
+
init_ui();
|
|
4019
|
+
DEFAULT_COLUMNS5 = [
|
|
4020
|
+
{ key: "matchId", label: "Match", maxWidth: 12 },
|
|
4021
|
+
{ key: "homeTeam", label: "Home", maxWidth: 20 },
|
|
4022
|
+
{ key: "awayTeam", label: "Away", maxWidth: 20 }
|
|
4023
|
+
];
|
|
4024
|
+
lineupCommand = defineCommand5({
|
|
4025
|
+
meta: {
|
|
4026
|
+
name: "lineup",
|
|
4027
|
+
description: "Fetch match lineups for a round"
|
|
501
4028
|
},
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
})
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
4029
|
+
args: {
|
|
4030
|
+
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
4031
|
+
round: { type: "string", description: "Round number", required: true },
|
|
4032
|
+
"match-id": { type: "string", description: "Specific match ID" },
|
|
4033
|
+
source: { type: "string", description: "Data source", default: "afl-api" },
|
|
4034
|
+
competition: {
|
|
4035
|
+
type: "string",
|
|
4036
|
+
description: "Competition code (AFLM or AFLW)",
|
|
4037
|
+
default: "AFLM"
|
|
4038
|
+
},
|
|
4039
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
4040
|
+
csv: { type: "boolean", description: "Output as CSV" },
|
|
4041
|
+
format: { type: "string", description: "Output format: table, json, csv" },
|
|
4042
|
+
full: { type: "boolean", description: "Show all columns in table output" }
|
|
4043
|
+
},
|
|
4044
|
+
async run({ args }) {
|
|
4045
|
+
const season = Number(args.season);
|
|
4046
|
+
const round = Number(args.round);
|
|
4047
|
+
const matchId = args["match-id"];
|
|
4048
|
+
const result = await withSpinner(
|
|
4049
|
+
"Fetching lineups\u2026",
|
|
4050
|
+
() => fetchLineup({
|
|
4051
|
+
source: args.source,
|
|
4052
|
+
season,
|
|
4053
|
+
round,
|
|
4054
|
+
matchId,
|
|
4055
|
+
competition: args.competition
|
|
4056
|
+
})
|
|
4057
|
+
);
|
|
4058
|
+
if (!result.success) {
|
|
4059
|
+
throw result.error;
|
|
4060
|
+
}
|
|
4061
|
+
const data = result.data;
|
|
4062
|
+
showSummary(`Loaded ${data.length} lineups for ${season} round ${round}`);
|
|
4063
|
+
const formatOptions = {
|
|
4064
|
+
json: args.json,
|
|
4065
|
+
csv: args.csv,
|
|
4066
|
+
format: args.format,
|
|
4067
|
+
full: args.full,
|
|
4068
|
+
columns: DEFAULT_COLUMNS5
|
|
4069
|
+
};
|
|
4070
|
+
console.log(formatOutput(data, formatOptions));
|
|
4071
|
+
}
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
533
4074
|
});
|
|
534
4075
|
|
|
535
4076
|
// src/cli/commands/squad.ts
|
|
536
|
-
var
|
|
537
|
-
__export(
|
|
4077
|
+
var squad_exports = {};
|
|
4078
|
+
__export(squad_exports, {
|
|
538
4079
|
squadCommand: () => squadCommand
|
|
539
4080
|
});
|
|
540
4081
|
import { defineCommand as defineCommand6 } from "citty";
|
|
541
4082
|
var DEFAULT_COLUMNS6, squadCommand;
|
|
542
|
-
var init_squad = __esm(
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
"team-id": { type: "string", description: "Team ID", required: true },
|
|
560
|
-
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
561
|
-
competition: {
|
|
562
|
-
type: "string",
|
|
563
|
-
description: "Competition code (AFLM or AFLW)",
|
|
564
|
-
default: "AFLM"
|
|
4083
|
+
var init_squad = __esm({
|
|
4084
|
+
"src/cli/commands/squad.ts"() {
|
|
4085
|
+
"use strict";
|
|
4086
|
+
init_index();
|
|
4087
|
+
init_formatters();
|
|
4088
|
+
init_ui();
|
|
4089
|
+
DEFAULT_COLUMNS6 = [
|
|
4090
|
+
{ key: "displayName", label: "Player", maxWidth: 24 },
|
|
4091
|
+
{ key: "jumperNumber", label: "#", maxWidth: 4 },
|
|
4092
|
+
{ key: "position", label: "Pos", maxWidth: 12 },
|
|
4093
|
+
{ key: "heightCm", label: "Ht", maxWidth: 5 },
|
|
4094
|
+
{ key: "weightKg", label: "Wt", maxWidth: 5 }
|
|
4095
|
+
];
|
|
4096
|
+
squadCommand = defineCommand6({
|
|
4097
|
+
meta: {
|
|
4098
|
+
name: "squad",
|
|
4099
|
+
description: "Fetch team squad for a season"
|
|
565
4100
|
},
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
4101
|
+
args: {
|
|
4102
|
+
"team-id": { type: "string", description: "Team ID", required: true },
|
|
4103
|
+
season: { type: "string", description: "Season year (e.g. 2025)", required: true },
|
|
4104
|
+
competition: {
|
|
4105
|
+
type: "string",
|
|
4106
|
+
description: "Competition code (AFLM or AFLW)",
|
|
4107
|
+
default: "AFLM"
|
|
4108
|
+
},
|
|
4109
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
4110
|
+
csv: { type: "boolean", description: "Output as CSV" },
|
|
4111
|
+
format: { type: "string", description: "Output format: table, json, csv" },
|
|
4112
|
+
full: { type: "boolean", description: "Show all columns in table output" }
|
|
4113
|
+
},
|
|
4114
|
+
async run({ args }) {
|
|
4115
|
+
const teamId = args["team-id"];
|
|
4116
|
+
const season = Number(args.season);
|
|
4117
|
+
const result = await withSpinner(
|
|
4118
|
+
"Fetching squad\u2026",
|
|
4119
|
+
() => fetchSquad({
|
|
4120
|
+
teamId,
|
|
4121
|
+
season,
|
|
4122
|
+
competition: args.competition
|
|
4123
|
+
})
|
|
4124
|
+
);
|
|
4125
|
+
if (!result.success) {
|
|
4126
|
+
throw result.error;
|
|
4127
|
+
}
|
|
4128
|
+
const data = result.data;
|
|
4129
|
+
showSummary(`Loaded ${data.players.length} players for ${data.teamName} ${season}`);
|
|
4130
|
+
const formatOptions = {
|
|
4131
|
+
json: args.json,
|
|
4132
|
+
csv: args.csv,
|
|
4133
|
+
format: args.format,
|
|
4134
|
+
full: args.full,
|
|
4135
|
+
columns: DEFAULT_COLUMNS6
|
|
4136
|
+
};
|
|
4137
|
+
console.log(formatOutput(data.players, formatOptions));
|
|
4138
|
+
}
|
|
4139
|
+
});
|
|
4140
|
+
}
|
|
594
4141
|
});
|
|
595
4142
|
|
|
596
4143
|
// src/cli/commands/teams.ts
|
|
597
|
-
var
|
|
598
|
-
__export(
|
|
4144
|
+
var teams_exports = {};
|
|
4145
|
+
__export(teams_exports, {
|
|
599
4146
|
teamsCommand: () => teamsCommand
|
|
600
4147
|
});
|
|
601
4148
|
import { defineCommand as defineCommand7 } from "citty";
|
|
602
4149
|
var DEFAULT_COLUMNS7, teamsCommand;
|
|
603
|
-
var
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
4150
|
+
var init_teams2 = __esm({
|
|
4151
|
+
"src/cli/commands/teams.ts"() {
|
|
4152
|
+
"use strict";
|
|
4153
|
+
init_index();
|
|
4154
|
+
init_formatters();
|
|
4155
|
+
init_ui();
|
|
4156
|
+
DEFAULT_COLUMNS7 = [
|
|
4157
|
+
{ key: "teamId", label: "ID", maxWidth: 8 },
|
|
4158
|
+
{ key: "name", label: "Team", maxWidth: 24 },
|
|
4159
|
+
{ key: "abbreviation", label: "Abbr", maxWidth: 6 },
|
|
4160
|
+
{ key: "competition", label: "Comp", maxWidth: 6 }
|
|
4161
|
+
];
|
|
4162
|
+
teamsCommand = defineCommand7({
|
|
4163
|
+
meta: {
|
|
4164
|
+
name: "teams",
|
|
4165
|
+
description: "Fetch team list"
|
|
4166
|
+
},
|
|
4167
|
+
args: {
|
|
4168
|
+
competition: { type: "string", description: "Competition code (AFLM or AFLW)" },
|
|
4169
|
+
"team-type": { type: "string", description: "Team type filter" },
|
|
4170
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
4171
|
+
csv: { type: "boolean", description: "Output as CSV" },
|
|
4172
|
+
format: { type: "string", description: "Output format: table, json, csv" },
|
|
4173
|
+
full: { type: "boolean", description: "Show all columns in table output" }
|
|
4174
|
+
},
|
|
4175
|
+
async run({ args }) {
|
|
4176
|
+
const result = await withSpinner(
|
|
4177
|
+
"Fetching teams\u2026",
|
|
4178
|
+
() => fetchTeams({
|
|
4179
|
+
competition: args.competition,
|
|
4180
|
+
teamType: args["team-type"]
|
|
4181
|
+
})
|
|
4182
|
+
);
|
|
4183
|
+
if (!result.success) {
|
|
4184
|
+
throw result.error;
|
|
4185
|
+
}
|
|
4186
|
+
const data = result.data;
|
|
4187
|
+
showSummary(`Loaded ${data.length} teams`);
|
|
4188
|
+
const formatOptions = {
|
|
4189
|
+
json: args.json,
|
|
4190
|
+
csv: args.csv,
|
|
4191
|
+
format: args.format,
|
|
4192
|
+
full: args.full,
|
|
4193
|
+
columns: DEFAULT_COLUMNS7
|
|
4194
|
+
};
|
|
4195
|
+
console.log(formatOutput(data, formatOptions));
|
|
4196
|
+
}
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
646
4199
|
});
|
|
647
4200
|
|
|
648
|
-
// src/cli.ts
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
4201
|
+
// src/cli/commands/team-stats.ts
|
|
4202
|
+
var team_stats_exports = {};
|
|
4203
|
+
__export(team_stats_exports, {
|
|
4204
|
+
teamStatsCommand: () => teamStatsCommand
|
|
4205
|
+
});
|
|
4206
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
4207
|
+
function flattenEntries(data) {
|
|
4208
|
+
return data.map((entry) => {
|
|
4209
|
+
const { stats, ...rest } = entry;
|
|
4210
|
+
return { ...rest, ...stats };
|
|
4211
|
+
});
|
|
660
4212
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
4213
|
+
var DEFAULT_COLUMNS8, teamStatsCommand;
|
|
4214
|
+
var init_team_stats2 = __esm({
|
|
4215
|
+
"src/cli/commands/team-stats.ts"() {
|
|
4216
|
+
"use strict";
|
|
4217
|
+
init_index();
|
|
4218
|
+
init_formatters();
|
|
4219
|
+
init_ui();
|
|
4220
|
+
DEFAULT_COLUMNS8 = [
|
|
4221
|
+
{ key: "team", label: "Team", maxWidth: 24 },
|
|
4222
|
+
{ key: "gamesPlayed", label: "GP", maxWidth: 5 }
|
|
4223
|
+
];
|
|
4224
|
+
teamStatsCommand = defineCommand8({
|
|
4225
|
+
meta: {
|
|
4226
|
+
name: "team-stats",
|
|
4227
|
+
description: "Fetch team aggregate statistics for a season"
|
|
4228
|
+
},
|
|
4229
|
+
args: {
|
|
4230
|
+
season: { type: "string", description: "Season year (e.g. 2024)", required: true },
|
|
4231
|
+
source: {
|
|
4232
|
+
type: "string",
|
|
4233
|
+
description: "Data source (footywire, afl-tables)",
|
|
4234
|
+
default: "footywire"
|
|
4235
|
+
},
|
|
4236
|
+
summary: { type: "string", description: "Summary type: totals or averages", default: "totals" },
|
|
4237
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
4238
|
+
csv: { type: "boolean", description: "Output as CSV" },
|
|
4239
|
+
format: { type: "string", description: "Output format: table, json, csv" },
|
|
4240
|
+
full: { type: "boolean", description: "Show all columns in table output" }
|
|
4241
|
+
},
|
|
4242
|
+
async run({ args }) {
|
|
4243
|
+
const season = Number(args.season);
|
|
4244
|
+
const summaryType = args.summary;
|
|
4245
|
+
const result = await withSpinner(
|
|
4246
|
+
"Fetching team stats\u2026",
|
|
4247
|
+
() => fetchTeamStats({
|
|
4248
|
+
source: args.source,
|
|
4249
|
+
season,
|
|
4250
|
+
summaryType
|
|
4251
|
+
})
|
|
4252
|
+
);
|
|
4253
|
+
if (!result.success) {
|
|
4254
|
+
throw result.error;
|
|
4255
|
+
}
|
|
4256
|
+
const data = result.data;
|
|
4257
|
+
showSummary(`Loaded stats for ${data.length} teams (${season}, ${summaryType})`);
|
|
4258
|
+
const flat = flattenEntries(data);
|
|
4259
|
+
const formatOptions = {
|
|
4260
|
+
json: args.json,
|
|
4261
|
+
csv: args.csv,
|
|
4262
|
+
format: args.format,
|
|
4263
|
+
full: args.full,
|
|
4264
|
+
columns: DEFAULT_COLUMNS8
|
|
4265
|
+
};
|
|
4266
|
+
console.log(formatOutput(flat, formatOptions));
|
|
4267
|
+
}
|
|
4268
|
+
});
|
|
668
4269
|
}
|
|
669
|
-
}
|
|
4270
|
+
});
|
|
670
4271
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
4272
|
+
// src/cli/commands/player-details.ts
|
|
4273
|
+
var player_details_exports = {};
|
|
4274
|
+
__export(player_details_exports, {
|
|
4275
|
+
playerDetailsCommand: () => playerDetailsCommand
|
|
4276
|
+
});
|
|
4277
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
4278
|
+
var DEFAULT_COLUMNS9, playerDetailsCommand;
|
|
4279
|
+
var init_player_details2 = __esm({
|
|
4280
|
+
"src/cli/commands/player-details.ts"() {
|
|
4281
|
+
"use strict";
|
|
4282
|
+
init_index();
|
|
4283
|
+
init_formatters();
|
|
4284
|
+
init_ui();
|
|
4285
|
+
DEFAULT_COLUMNS9 = [
|
|
4286
|
+
{ key: "displayName", label: "Player", maxWidth: 24 },
|
|
4287
|
+
{ key: "jumperNumber", label: "#", maxWidth: 4 },
|
|
4288
|
+
{ key: "position", label: "Pos", maxWidth: 12 },
|
|
4289
|
+
{ key: "heightCm", label: "Ht", maxWidth: 5 },
|
|
4290
|
+
{ key: "weightKg", label: "Wt", maxWidth: 5 },
|
|
4291
|
+
{ key: "gamesPlayed", label: "Games", maxWidth: 6 },
|
|
4292
|
+
{ key: "dateOfBirth", label: "DOB", maxWidth: 12 }
|
|
4293
|
+
];
|
|
4294
|
+
playerDetailsCommand = defineCommand9({
|
|
4295
|
+
meta: {
|
|
4296
|
+
name: "player-details",
|
|
4297
|
+
description: "Fetch player biographical details for a team"
|
|
4298
|
+
},
|
|
4299
|
+
args: {
|
|
4300
|
+
team: { type: "positional", description: "Team name (e.g. Carlton, Hawthorn)", required: true },
|
|
4301
|
+
source: {
|
|
4302
|
+
type: "string",
|
|
4303
|
+
description: "Data source: afl-api, footywire, afl-tables",
|
|
4304
|
+
default: "afl-api"
|
|
4305
|
+
},
|
|
4306
|
+
season: { type: "string", description: "Season year (for AFL API source, e.g. 2025)" },
|
|
4307
|
+
competition: {
|
|
4308
|
+
type: "string",
|
|
4309
|
+
description: "Competition code (AFLM or AFLW)",
|
|
4310
|
+
default: "AFLM"
|
|
4311
|
+
},
|
|
4312
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
4313
|
+
csv: { type: "boolean", description: "Output as CSV" },
|
|
4314
|
+
format: { type: "string", description: "Output format: table, json, csv" },
|
|
4315
|
+
full: { type: "boolean", description: "Show all columns in table output" }
|
|
4316
|
+
},
|
|
4317
|
+
async run({ args }) {
|
|
4318
|
+
const source = args.source;
|
|
4319
|
+
const season = args.season ? Number(args.season) : void 0;
|
|
4320
|
+
const result = await withSpinner(
|
|
4321
|
+
"Fetching player details\u2026",
|
|
4322
|
+
() => fetchPlayerDetails({
|
|
4323
|
+
source,
|
|
4324
|
+
team: args.team,
|
|
4325
|
+
season,
|
|
4326
|
+
competition: args.competition
|
|
4327
|
+
})
|
|
4328
|
+
);
|
|
4329
|
+
if (!result.success) {
|
|
4330
|
+
throw result.error;
|
|
4331
|
+
}
|
|
4332
|
+
const data = result.data;
|
|
4333
|
+
showSummary(`Loaded ${data.length} players for ${args.team} (${source})`);
|
|
4334
|
+
const formatOptions = {
|
|
4335
|
+
json: args.json,
|
|
4336
|
+
csv: args.csv,
|
|
4337
|
+
format: args.format,
|
|
4338
|
+
full: args.full,
|
|
4339
|
+
columns: DEFAULT_COLUMNS9
|
|
4340
|
+
};
|
|
4341
|
+
console.log(formatOutput(data, formatOptions));
|
|
4342
|
+
}
|
|
4343
|
+
});
|
|
677
4344
|
}
|
|
678
|
-
}
|
|
4345
|
+
});
|
|
679
4346
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
4347
|
+
// src/cli/commands/coaches-votes.ts
|
|
4348
|
+
var coaches_votes_exports = {};
|
|
4349
|
+
__export(coaches_votes_exports, {
|
|
4350
|
+
coachesVotesCommand: () => coachesVotesCommand
|
|
4351
|
+
});
|
|
4352
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
4353
|
+
var DEFAULT_COLUMNS10, coachesVotesCommand;
|
|
4354
|
+
var init_coaches_votes2 = __esm({
|
|
4355
|
+
"src/cli/commands/coaches-votes.ts"() {
|
|
4356
|
+
"use strict";
|
|
4357
|
+
init_index();
|
|
4358
|
+
init_formatters();
|
|
4359
|
+
init_ui();
|
|
4360
|
+
DEFAULT_COLUMNS10 = [
|
|
4361
|
+
{ key: "season", label: "Season", maxWidth: 8 },
|
|
4362
|
+
{ key: "round", label: "Round", maxWidth: 6 },
|
|
4363
|
+
{ key: "homeTeam", label: "Home", maxWidth: 20 },
|
|
4364
|
+
{ key: "awayTeam", label: "Away", maxWidth: 20 },
|
|
4365
|
+
{ key: "playerName", label: "Player", maxWidth: 30 },
|
|
4366
|
+
{ key: "votes", label: "Votes", maxWidth: 6 }
|
|
4367
|
+
];
|
|
4368
|
+
coachesVotesCommand = defineCommand10({
|
|
4369
|
+
meta: {
|
|
4370
|
+
name: "coaches-votes",
|
|
4371
|
+
description: "Fetch AFLCA coaches votes for a season"
|
|
4372
|
+
},
|
|
4373
|
+
args: {
|
|
4374
|
+
season: { type: "string", description: "Season year (e.g. 2024)", required: true },
|
|
4375
|
+
round: { type: "string", description: "Round number" },
|
|
4376
|
+
competition: {
|
|
4377
|
+
type: "string",
|
|
4378
|
+
description: "Competition code (AFLM or AFLW)",
|
|
4379
|
+
default: "AFLM"
|
|
4380
|
+
},
|
|
4381
|
+
team: { type: "string", description: "Filter by team name" },
|
|
4382
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
4383
|
+
csv: { type: "boolean", description: "Output as CSV" },
|
|
4384
|
+
format: { type: "string", description: "Output format: table, json, csv" },
|
|
4385
|
+
full: { type: "boolean", description: "Show all columns in table output" }
|
|
4386
|
+
},
|
|
4387
|
+
async run({ args }) {
|
|
4388
|
+
const season = Number(args.season);
|
|
4389
|
+
const round = args.round ? Number(args.round) : void 0;
|
|
4390
|
+
const result = await withSpinner(
|
|
4391
|
+
"Fetching coaches votes\u2026",
|
|
4392
|
+
() => fetchCoachesVotes({
|
|
4393
|
+
season,
|
|
4394
|
+
round,
|
|
4395
|
+
competition: args.competition,
|
|
4396
|
+
team: args.team
|
|
4397
|
+
})
|
|
4398
|
+
);
|
|
4399
|
+
if (!result.success) {
|
|
4400
|
+
throw result.error;
|
|
4401
|
+
}
|
|
4402
|
+
const data = result.data;
|
|
4403
|
+
const teamSuffix = args.team ? ` for ${args.team}` : "";
|
|
4404
|
+
const roundSuffix = round ? ` round ${round}` : "";
|
|
4405
|
+
showSummary(`Loaded ${data.length} vote records for ${season}${roundSuffix}${teamSuffix}`);
|
|
4406
|
+
const formatOptions = {
|
|
4407
|
+
json: args.json,
|
|
4408
|
+
csv: args.csv,
|
|
4409
|
+
format: args.format,
|
|
4410
|
+
full: args.full,
|
|
4411
|
+
columns: DEFAULT_COLUMNS10
|
|
4412
|
+
};
|
|
4413
|
+
console.log(formatOutput(data, formatOptions));
|
|
4414
|
+
}
|
|
4415
|
+
});
|
|
686
4416
|
}
|
|
687
|
-
}
|
|
4417
|
+
});
|
|
688
4418
|
|
|
689
4419
|
// src/cli.ts
|
|
690
|
-
|
|
4420
|
+
init_errors();
|
|
4421
|
+
import { defineCommand as defineCommand11, runMain } from "citty";
|
|
4422
|
+
import pc2 from "picocolors";
|
|
4423
|
+
var main = defineCommand11({
|
|
691
4424
|
meta: {
|
|
692
4425
|
name: "fitzroy",
|
|
693
|
-
version: "1.0
|
|
694
|
-
description: "CLI for fetching AFL data
|
|
4426
|
+
version: "1.1.0",
|
|
4427
|
+
description: "CLI for fetching AFL data \u2014 match results, player stats, fixtures, ladders, and more"
|
|
695
4428
|
},
|
|
696
4429
|
subCommands: {
|
|
697
|
-
matches: () => Promise.resolve().then(() => (init_matches(),
|
|
698
|
-
stats: () => Promise.resolve().then(() => (init_stats(),
|
|
699
|
-
fixture: () => Promise.resolve().then(() => (
|
|
700
|
-
ladder: () => Promise.resolve().then(() => (
|
|
701
|
-
lineup: () => Promise.resolve().then(() => (
|
|
702
|
-
squad: () => Promise.resolve().then(() => (init_squad(),
|
|
703
|
-
teams: () => Promise.resolve().then(() => (
|
|
4430
|
+
matches: () => Promise.resolve().then(() => (init_matches(), matches_exports)).then((m) => m.matchesCommand),
|
|
4431
|
+
stats: () => Promise.resolve().then(() => (init_stats(), stats_exports)).then((m) => m.statsCommand),
|
|
4432
|
+
fixture: () => Promise.resolve().then(() => (init_fixture2(), fixture_exports)).then((m) => m.fixtureCommand),
|
|
4433
|
+
ladder: () => Promise.resolve().then(() => (init_ladder3(), ladder_exports)).then((m) => m.ladderCommand),
|
|
4434
|
+
lineup: () => Promise.resolve().then(() => (init_lineup3(), lineup_exports)).then((m) => m.lineupCommand),
|
|
4435
|
+
squad: () => Promise.resolve().then(() => (init_squad(), squad_exports)).then((m) => m.squadCommand),
|
|
4436
|
+
teams: () => Promise.resolve().then(() => (init_teams2(), teams_exports)).then((m) => m.teamsCommand),
|
|
4437
|
+
"team-stats": () => Promise.resolve().then(() => (init_team_stats2(), team_stats_exports)).then((m) => m.teamStatsCommand),
|
|
4438
|
+
"player-details": () => Promise.resolve().then(() => (init_player_details2(), player_details_exports)).then((m) => m.playerDetailsCommand),
|
|
4439
|
+
"coaches-votes": () => Promise.resolve().then(() => (init_coaches_votes2(), coaches_votes_exports)).then((m) => m.coachesVotesCommand)
|
|
704
4440
|
}
|
|
705
4441
|
});
|
|
706
4442
|
function formatError(error) {
|
|
707
4443
|
if (error instanceof ValidationError && error.issues) {
|
|
708
4444
|
const issueLines = error.issues.map((i) => ` ${pc2.yellow(i.path)}: ${i.message}`);
|
|
709
4445
|
return `${pc2.red("Validation error:")}
|
|
710
|
-
${issueLines.join(
|
|
711
|
-
`)}`;
|
|
4446
|
+
${issueLines.join("\n")}`;
|
|
712
4447
|
}
|
|
713
4448
|
if (error instanceof AflApiError) {
|
|
714
4449
|
const status = error.statusCode ? ` (HTTP ${error.statusCode})` : "";
|