fitzroy 1.0.2 → 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 +2630 -673
- package/dist/index.d.ts +1372 -914
- package/dist/index.js +2874 -1056
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -58,18 +58,118 @@ var init_result = __esm({
|
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
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
|
+
|
|
61
161
|
// src/lib/team-mapping.ts
|
|
62
162
|
function normaliseTeamName(raw) {
|
|
63
163
|
const trimmed = raw.trim();
|
|
64
164
|
return ALIAS_MAP.get(trimmed.toLowerCase()) ?? trimmed;
|
|
65
165
|
}
|
|
66
|
-
var TEAM_ALIASES, ALIAS_MAP;
|
|
166
|
+
var TEAM_ALIASES, AFL_SENIOR_TEAMS, ALIAS_MAP;
|
|
67
167
|
var init_team_mapping = __esm({
|
|
68
168
|
"src/lib/team-mapping.ts"() {
|
|
69
169
|
"use strict";
|
|
70
170
|
TEAM_ALIASES = [
|
|
71
171
|
["Adelaide Crows", "Adelaide", "Crows", "ADEL", "AD"],
|
|
72
|
-
["Brisbane Lions", "Brisbane", "Brisbane Bears", "Bears", "Fitzroy Lions", "BL", "BRIS"],
|
|
172
|
+
["Brisbane Lions", "Brisbane", "Brisbane Bears", "Bears", "Lions", "Fitzroy Lions", "BL", "BRIS"],
|
|
73
173
|
["Carlton", "Carlton Blues", "Blues", "CARL", "CA"],
|
|
74
174
|
["Collingwood", "Collingwood Magpies", "Magpies", "COLL", "CW"],
|
|
75
175
|
["Essendon", "Essendon Bombers", "Bombers", "ESS", "ES"],
|
|
@@ -106,6 +206,26 @@ var init_team_mapping = __esm({
|
|
|
106
206
|
["Fitzroy", "Fitzroy Reds", "Fitzroy Gorillas", "Fitzroy Maroons", "FI"],
|
|
107
207
|
["University", "University Blacks"]
|
|
108
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
|
+
]);
|
|
109
229
|
ALIAS_MAP = (() => {
|
|
110
230
|
const map = /* @__PURE__ */ new Map();
|
|
111
231
|
for (const [canonical, ...aliases] of TEAM_ALIASES) {
|
|
@@ -119,160 +239,1211 @@ var init_team_mapping = __esm({
|
|
|
119
239
|
}
|
|
120
240
|
});
|
|
121
241
|
|
|
122
|
-
// src/lib/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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"() {
|
|
127
259
|
"use strict";
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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(),
|
|
276
1447
|
hitoutsToAdvantage: z.number().optional(),
|
|
277
1448
|
hitoutWinPercentage: z.number().optional(),
|
|
278
1449
|
hitoutToAdvantageRate: z.number().optional(),
|
|
@@ -755,96 +1926,233 @@ var init_afl_api = __esm({
|
|
|
755
1926
|
if (roundId != null) {
|
|
756
1927
|
url += `?roundId=${roundId}`;
|
|
757
1928
|
}
|
|
758
|
-
return this.fetchJson(url, LadderResponseSchema);
|
|
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);
|
|
759
2074
|
}
|
|
760
2075
|
};
|
|
761
2076
|
}
|
|
762
2077
|
});
|
|
763
2078
|
|
|
764
|
-
// src/transforms/
|
|
765
|
-
function
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
switch (raw) {
|
|
770
|
-
case "CONCLUDED":
|
|
771
|
-
case "COMPLETE":
|
|
772
|
-
return "Complete";
|
|
773
|
-
case "LIVE":
|
|
774
|
-
case "IN_PROGRESS":
|
|
775
|
-
return "Live";
|
|
776
|
-
case "UPCOMING":
|
|
777
|
-
case "SCHEDULED":
|
|
778
|
-
return "Upcoming";
|
|
779
|
-
case "POSTPONED":
|
|
780
|
-
return "Postponed";
|
|
781
|
-
case "CANCELLED":
|
|
782
|
-
return "Cancelled";
|
|
783
|
-
default:
|
|
784
|
-
return "Complete";
|
|
785
|
-
}
|
|
2079
|
+
// src/transforms/squiggle.ts
|
|
2080
|
+
function toMatchStatus2(complete) {
|
|
2081
|
+
if (complete === 100) return "Complete";
|
|
2082
|
+
if (complete > 0) return "Live";
|
|
2083
|
+
return "Upcoming";
|
|
786
2084
|
}
|
|
787
|
-
function
|
|
788
|
-
return {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
+
}));
|
|
793
2121
|
}
|
|
794
|
-
function
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
+
}));
|
|
798
2135
|
}
|
|
799
|
-
function
|
|
800
|
-
return
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
awayTeam: normaliseTeamName(item.match.awayTeam.name),
|
|
814
|
-
homeGoals: homeScore?.matchScore.goals ?? 0,
|
|
815
|
-
homeBehinds: homeScore?.matchScore.behinds ?? 0,
|
|
816
|
-
homePoints,
|
|
817
|
-
awayGoals: awayScore?.matchScore.goals ?? 0,
|
|
818
|
-
awayBehinds: awayScore?.matchScore.behinds ?? 0,
|
|
819
|
-
awayPoints,
|
|
820
|
-
margin: homePoints - awayPoints,
|
|
821
|
-
q1Home: findPeriod(homeScore?.periodScore, 1),
|
|
822
|
-
q2Home: findPeriod(homeScore?.periodScore, 2),
|
|
823
|
-
q3Home: findPeriod(homeScore?.periodScore, 3),
|
|
824
|
-
q4Home: findPeriod(homeScore?.periodScore, 4),
|
|
825
|
-
q1Away: findPeriod(awayScore?.periodScore, 1),
|
|
826
|
-
q2Away: findPeriod(awayScore?.periodScore, 2),
|
|
827
|
-
q3Away: findPeriod(awayScore?.periodScore, 3),
|
|
828
|
-
q4Away: findPeriod(awayScore?.periodScore, 4),
|
|
829
|
-
status: toMatchStatus(item.match.status),
|
|
830
|
-
attendance: null,
|
|
831
|
-
venueState: item.venue?.state ?? null,
|
|
832
|
-
venueTimezone: item.venue?.timeZone ?? null,
|
|
833
|
-
homeRushedBehinds: homeScore?.rushedBehinds ?? null,
|
|
834
|
-
awayRushedBehinds: awayScore?.rushedBehinds ?? null,
|
|
835
|
-
homeMinutesInFront: homeScore?.minutesInFront ?? null,
|
|
836
|
-
awayMinutesInFront: awayScore?.minutesInFront ?? null,
|
|
837
|
-
source,
|
|
838
|
-
competition
|
|
839
|
-
};
|
|
840
|
-
});
|
|
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
|
+
}));
|
|
841
2150
|
}
|
|
842
|
-
var
|
|
843
|
-
|
|
844
|
-
"src/transforms/match-results.ts"() {
|
|
2151
|
+
var init_squiggle2 = __esm({
|
|
2152
|
+
"src/transforms/squiggle.ts"() {
|
|
845
2153
|
"use strict";
|
|
846
2154
|
init_team_mapping();
|
|
847
|
-
|
|
2155
|
+
init_match_results();
|
|
848
2156
|
}
|
|
849
2157
|
});
|
|
850
2158
|
|
|
@@ -865,10 +2173,25 @@ function toFixture(item, season, fallbackRoundNumber, competition) {
|
|
|
865
2173
|
}
|
|
866
2174
|
async function fetchFixture(query) {
|
|
867
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
|
+
}
|
|
868
2191
|
if (query.source !== "afl-api") {
|
|
869
2192
|
return err(
|
|
870
2193
|
new UnsupportedSourceError(
|
|
871
|
-
"Fixture data is only available from the AFL API
|
|
2194
|
+
"Fixture data is only available from the AFL API, FootyWire, or Squiggle sources.",
|
|
872
2195
|
query.source
|
|
873
2196
|
)
|
|
874
2197
|
);
|
|
@@ -888,289 +2211,160 @@ async function fetchFixture(query) {
|
|
|
888
2211
|
);
|
|
889
2212
|
const roundResults = await Promise.all(
|
|
890
2213
|
roundProviderIds.map((r) => client.fetchRoundMatchItems(r.providerId))
|
|
891
|
-
);
|
|
892
|
-
const fixtures = [];
|
|
893
|
-
for (let i = 0; i < roundResults.length; i++) {
|
|
894
|
-
const result = roundResults[i];
|
|
895
|
-
if (!result?.success) continue;
|
|
896
|
-
const roundNumber = roundProviderIds[i]?.roundNumber ?? 0;
|
|
897
|
-
for (const item of result.data) {
|
|
898
|
-
fixtures.push(toFixture(item, query.season, roundNumber, competition));
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
return ok(fixtures);
|
|
902
|
-
}
|
|
903
|
-
var init_fixture = __esm({
|
|
904
|
-
"src/api/fixture.ts"() {
|
|
905
|
-
"use strict";
|
|
906
|
-
init_errors();
|
|
907
|
-
init_result();
|
|
908
|
-
init_team_mapping();
|
|
909
|
-
init_afl_api();
|
|
910
|
-
init_match_results();
|
|
911
|
-
}
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
// src/transforms/ladder.ts
|
|
915
|
-
function transformLadderEntries(entries) {
|
|
916
|
-
return entries.map((entry) => {
|
|
917
|
-
const record = entry.thisSeasonRecord;
|
|
918
|
-
const wl = record?.winLossRecord;
|
|
919
|
-
return {
|
|
920
|
-
position: entry.position,
|
|
921
|
-
team: normaliseTeamName(entry.team.name),
|
|
922
|
-
played: entry.played ?? wl?.played ?? 0,
|
|
923
|
-
wins: wl?.wins ?? 0,
|
|
924
|
-
losses: wl?.losses ?? 0,
|
|
925
|
-
draws: wl?.draws ?? 0,
|
|
926
|
-
pointsFor: entry.pointsFor ?? 0,
|
|
927
|
-
pointsAgainst: entry.pointsAgainst ?? 0,
|
|
928
|
-
percentage: record?.percentage ?? 0,
|
|
929
|
-
premiershipsPoints: record?.aggregatePoints ?? 0,
|
|
930
|
-
form: entry.form ?? null
|
|
931
|
-
};
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
var init_ladder = __esm({
|
|
935
|
-
"src/transforms/ladder.ts"() {
|
|
936
|
-
"use strict";
|
|
937
|
-
init_team_mapping();
|
|
938
|
-
}
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
// src/api/ladder.ts
|
|
942
|
-
async function fetchLadder(query) {
|
|
943
|
-
const competition = query.competition ?? "AFLM";
|
|
944
|
-
if (query.source !== "afl-api") {
|
|
945
|
-
return err(
|
|
946
|
-
new UnsupportedSourceError(
|
|
947
|
-
"Ladder data is only available from the AFL API source.",
|
|
948
|
-
query.source
|
|
949
|
-
)
|
|
950
|
-
);
|
|
951
|
-
}
|
|
952
|
-
const client = new AflApiClient();
|
|
953
|
-
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
954
|
-
if (!seasonResult.success) return seasonResult;
|
|
955
|
-
let roundId;
|
|
956
|
-
if (query.round != null) {
|
|
957
|
-
const roundsResult = await client.resolveRounds(seasonResult.data);
|
|
958
|
-
if (!roundsResult.success) return roundsResult;
|
|
959
|
-
const round = roundsResult.data.find((r) => r.roundNumber === query.round);
|
|
960
|
-
if (round) {
|
|
961
|
-
roundId = round.id;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
const ladderResult = await client.fetchLadder(seasonResult.data, roundId);
|
|
965
|
-
if (!ladderResult.success) return ladderResult;
|
|
966
|
-
const firstLadder = ladderResult.data.ladders[0];
|
|
967
|
-
const entries = firstLadder ? transformLadderEntries(firstLadder.entries) : [];
|
|
968
|
-
return ok({
|
|
969
|
-
season: query.season,
|
|
970
|
-
roundNumber: ladderResult.data.round?.roundNumber ?? null,
|
|
971
|
-
entries,
|
|
972
|
-
competition
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
var init_ladder2 = __esm({
|
|
976
|
-
"src/api/ladder.ts"() {
|
|
977
|
-
"use strict";
|
|
978
|
-
init_errors();
|
|
979
|
-
init_result();
|
|
980
|
-
init_afl_api();
|
|
981
|
-
init_ladder();
|
|
982
|
-
}
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
// src/transforms/lineup.ts
|
|
986
|
-
function transformMatchRoster(roster, season, roundNumber, competition) {
|
|
987
|
-
const homeTeamId = roster.match.homeTeamId;
|
|
988
|
-
const awayTeamId = roster.match.awayTeamId;
|
|
989
|
-
const homeTeamPlayers = roster.teamPlayers.find((tp) => tp.teamId === homeTeamId);
|
|
990
|
-
const awayTeamPlayers = roster.teamPlayers.find((tp) => tp.teamId === awayTeamId);
|
|
991
|
-
const mapPlayers = (players) => players.map((p) => {
|
|
992
|
-
const inner = p.player.player;
|
|
993
|
-
const position = p.player.position ?? null;
|
|
994
|
-
return {
|
|
995
|
-
playerId: inner.playerId,
|
|
996
|
-
givenName: inner.playerName.givenName,
|
|
997
|
-
surname: inner.playerName.surname,
|
|
998
|
-
displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
|
|
999
|
-
jumperNumber: p.jumperNumber ?? null,
|
|
1000
|
-
position,
|
|
1001
|
-
isEmergency: position !== null && EMERGENCY_POSITIONS.has(position),
|
|
1002
|
-
isSubstitute: position !== null && SUBSTITUTE_POSITIONS.has(position)
|
|
1003
|
-
};
|
|
1004
|
-
});
|
|
1005
|
-
return {
|
|
1006
|
-
matchId: roster.match.matchId,
|
|
1007
|
-
season,
|
|
1008
|
-
roundNumber,
|
|
1009
|
-
homeTeam: normaliseTeamName(roster.match.homeTeam.name),
|
|
1010
|
-
awayTeam: normaliseTeamName(roster.match.awayTeam.name),
|
|
1011
|
-
homePlayers: homeTeamPlayers ? mapPlayers(homeTeamPlayers.players) : [],
|
|
1012
|
-
awayPlayers: awayTeamPlayers ? mapPlayers(awayTeamPlayers.players) : [],
|
|
1013
|
-
competition
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
var EMERGENCY_POSITIONS, SUBSTITUTE_POSITIONS;
|
|
1017
|
-
var init_lineup = __esm({
|
|
1018
|
-
"src/transforms/lineup.ts"() {
|
|
1019
|
-
"use strict";
|
|
1020
|
-
init_team_mapping();
|
|
1021
|
-
EMERGENCY_POSITIONS = /* @__PURE__ */ new Set(["EMG", "EMERG"]);
|
|
1022
|
-
SUBSTITUTE_POSITIONS = /* @__PURE__ */ new Set(["SUB", "INT"]);
|
|
1023
|
-
}
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
// src/api/lineup.ts
|
|
1027
|
-
async function fetchLineup(query) {
|
|
1028
|
-
const competition = query.competition ?? "AFLM";
|
|
1029
|
-
if (query.source !== "afl-api") {
|
|
1030
|
-
return err(
|
|
1031
|
-
new UnsupportedSourceError(
|
|
1032
|
-
"Lineup data is only available from the AFL API source.",
|
|
1033
|
-
query.source
|
|
1034
|
-
)
|
|
1035
|
-
);
|
|
1036
|
-
}
|
|
1037
|
-
const client = new AflApiClient();
|
|
1038
|
-
if (query.matchId) {
|
|
1039
|
-
const rosterResult = await client.fetchMatchRoster(query.matchId);
|
|
1040
|
-
if (!rosterResult.success) return rosterResult;
|
|
1041
|
-
return ok([transformMatchRoster(rosterResult.data, query.season, query.round, competition)]);
|
|
1042
|
-
}
|
|
1043
|
-
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
1044
|
-
if (!seasonResult.success) return seasonResult;
|
|
1045
|
-
const matchItems = await client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round);
|
|
1046
|
-
if (!matchItems.success) return matchItems;
|
|
1047
|
-
if (matchItems.data.length === 0) {
|
|
1048
|
-
return err(new AflApiError(`No matches found for round ${query.round}`));
|
|
1049
|
-
}
|
|
1050
|
-
const rosterResults = await Promise.all(
|
|
1051
|
-
matchItems.data.map((item) => client.fetchMatchRoster(item.match.matchId))
|
|
1052
|
-
);
|
|
1053
|
-
const lineups = [];
|
|
1054
|
-
for (const rosterResult of rosterResults) {
|
|
1055
|
-
if (!rosterResult.success) return rosterResult;
|
|
1056
|
-
lineups.push(transformMatchRoster(rosterResult.data, query.season, query.round, competition));
|
|
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
|
+
}
|
|
1057
2223
|
}
|
|
1058
|
-
return ok(
|
|
2224
|
+
return ok(fixtures);
|
|
1059
2225
|
}
|
|
1060
|
-
var
|
|
1061
|
-
"src/api/
|
|
2226
|
+
var init_fixture = __esm({
|
|
2227
|
+
"src/api/fixture.ts"() {
|
|
1062
2228
|
"use strict";
|
|
1063
2229
|
init_errors();
|
|
1064
2230
|
init_result();
|
|
2231
|
+
init_team_mapping();
|
|
1065
2232
|
init_afl_api();
|
|
1066
|
-
|
|
2233
|
+
init_footywire();
|
|
2234
|
+
init_squiggle();
|
|
2235
|
+
init_match_results();
|
|
2236
|
+
init_squiggle2();
|
|
1067
2237
|
}
|
|
1068
2238
|
});
|
|
1069
2239
|
|
|
1070
|
-
// src/
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
const [, dayStr, monthStr, yearStr] = match;
|
|
1083
|
-
if (!dayStr || !monthStr || !yearStr) {
|
|
1084
|
-
return null;
|
|
1085
|
-
}
|
|
1086
|
-
const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
|
|
1087
|
-
if (monthIndex === void 0) {
|
|
1088
|
-
return null;
|
|
1089
|
-
}
|
|
1090
|
-
const year = Number.parseInt(yearStr, 10);
|
|
1091
|
-
const day = Number.parseInt(dayStr, 10);
|
|
1092
|
-
const date = new Date(Date.UTC(year, monthIndex, day));
|
|
1093
|
-
if (Number.isNaN(date.getTime())) {
|
|
1094
|
-
return null;
|
|
1095
|
-
}
|
|
1096
|
-
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
|
|
1097
|
-
return null;
|
|
1098
|
-
}
|
|
1099
|
-
return date;
|
|
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
|
+
};
|
|
1100
2252
|
}
|
|
1101
|
-
function
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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;
|
|
1123
2344
|
}
|
|
1124
|
-
function
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
return null;
|
|
1135
|
-
}
|
|
1136
|
-
return date;
|
|
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;
|
|
1137
2355
|
}
|
|
1138
|
-
var
|
|
1139
|
-
|
|
1140
|
-
"src/lib/date-utils.ts"() {
|
|
2356
|
+
var init_afl_tables_player_stats = __esm({
|
|
2357
|
+
"src/transforms/afl-tables-player-stats.ts"() {
|
|
1141
2358
|
"use strict";
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
["feb", 1],
|
|
1145
|
-
["mar", 2],
|
|
1146
|
-
["apr", 3],
|
|
1147
|
-
["may", 4],
|
|
1148
|
-
["jun", 5],
|
|
1149
|
-
["jul", 6],
|
|
1150
|
-
["aug", 7],
|
|
1151
|
-
["sep", 8],
|
|
1152
|
-
["oct", 9],
|
|
1153
|
-
["nov", 10],
|
|
1154
|
-
["dec", 11],
|
|
1155
|
-
["january", 0],
|
|
1156
|
-
["february", 1],
|
|
1157
|
-
["march", 2],
|
|
1158
|
-
["april", 3],
|
|
1159
|
-
["june", 5],
|
|
1160
|
-
["july", 6],
|
|
1161
|
-
["august", 7],
|
|
1162
|
-
["september", 8],
|
|
1163
|
-
["october", 9],
|
|
1164
|
-
["november", 10],
|
|
1165
|
-
["december", 11]
|
|
1166
|
-
]);
|
|
2359
|
+
init_parse_utils();
|
|
2360
|
+
init_team_mapping();
|
|
1167
2361
|
}
|
|
1168
2362
|
});
|
|
1169
2363
|
|
|
1170
2364
|
// src/sources/afl-tables.ts
|
|
1171
|
-
import * as
|
|
2365
|
+
import * as cheerio5 from "cheerio";
|
|
1172
2366
|
function parseSeasonPage(html, year) {
|
|
1173
|
-
const $ =
|
|
2367
|
+
const $ = cheerio5.load(html);
|
|
1174
2368
|
const results = [];
|
|
1175
2369
|
let currentRound = 0;
|
|
1176
2370
|
let currentRoundType = "HomeAndAway";
|
|
@@ -1178,17 +2372,18 @@ function parseSeasonPage(html, year) {
|
|
|
1178
2372
|
$("table").each((_i, table) => {
|
|
1179
2373
|
const $table = $(table);
|
|
1180
2374
|
const text = $table.text().trim();
|
|
2375
|
+
const border = $table.attr("border");
|
|
1181
2376
|
const roundMatch = /^Round\s+(\d+)/i.exec(text);
|
|
1182
|
-
if (roundMatch?.[1] &&
|
|
2377
|
+
if (roundMatch?.[1] && border !== "1") {
|
|
1183
2378
|
currentRound = Number.parseInt(roundMatch[1], 10);
|
|
1184
2379
|
currentRoundType = inferRoundType(text);
|
|
1185
2380
|
return;
|
|
1186
2381
|
}
|
|
1187
|
-
if (
|
|
2382
|
+
if (border !== "1" && inferRoundType(text) === "Finals") {
|
|
1188
2383
|
currentRoundType = "Finals";
|
|
1189
2384
|
return;
|
|
1190
2385
|
}
|
|
1191
|
-
if (
|
|
2386
|
+
if (border !== "1") return;
|
|
1192
2387
|
const rows = $table.find("tr");
|
|
1193
2388
|
if (rows.length !== 2) return;
|
|
1194
2389
|
const homeRow = $(rows[0]);
|
|
@@ -1265,7 +2460,7 @@ function parseDateFromInfo(text, year) {
|
|
|
1265
2460
|
return parseAflTablesDate(text) ?? new Date(year, 0, 1);
|
|
1266
2461
|
}
|
|
1267
2462
|
function parseVenueFromInfo(html) {
|
|
1268
|
-
const $ =
|
|
2463
|
+
const $ = cheerio5.load(html);
|
|
1269
2464
|
const venueLink = $("a[href*='venues']");
|
|
1270
2465
|
if (venueLink.length > 0) {
|
|
1271
2466
|
return venueLink.text().trim();
|
|
@@ -1278,7 +2473,108 @@ function parseAttendanceFromInfo(text) {
|
|
|
1278
2473
|
if (!match?.[1]) return null;
|
|
1279
2474
|
return Number.parseInt(match[1].replace(/,/g, ""), 10) || null;
|
|
1280
2475
|
}
|
|
1281
|
-
|
|
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;
|
|
1282
2578
|
var init_afl_tables = __esm({
|
|
1283
2579
|
"src/sources/afl-tables.ts"() {
|
|
1284
2580
|
"use strict";
|
|
@@ -1286,6 +2582,7 @@ var init_afl_tables = __esm({
|
|
|
1286
2582
|
init_errors();
|
|
1287
2583
|
init_result();
|
|
1288
2584
|
init_team_mapping();
|
|
2585
|
+
init_afl_tables_player_stats();
|
|
1289
2586
|
init_match_results();
|
|
1290
2587
|
AFL_TABLES_BASE = "https://afltables.com/afl/seas";
|
|
1291
2588
|
AflTablesClient = class {
|
|
@@ -1301,178 +2598,440 @@ var init_afl_tables = __esm({
|
|
|
1301
2598
|
*/
|
|
1302
2599
|
async fetchSeasonResults(year) {
|
|
1303
2600
|
const url = `${AFL_TABLES_BASE}/${year}.html`;
|
|
1304
|
-
try {
|
|
1305
|
-
const response = await this.fetchFn(url, {
|
|
1306
|
-
headers: { "User-Agent": "Mozilla/5.0" }
|
|
1307
|
-
});
|
|
1308
|
-
if (!response.ok) {
|
|
1309
|
-
return err(
|
|
1310
|
-
new ScrapeError(`AFL Tables request failed: ${response.status} (${url})`, "afl-tables")
|
|
1311
|
-
);
|
|
1312
|
-
}
|
|
1313
|
-
const html = await response.text();
|
|
1314
|
-
const results = parseSeasonPage(html, year);
|
|
1315
|
-
return ok(results);
|
|
1316
|
-
} catch (cause) {
|
|
1317
|
-
return err(
|
|
1318
|
-
new ScrapeError(
|
|
1319
|
-
`AFL Tables request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1320
|
-
"afl-tables"
|
|
1321
|
-
)
|
|
1322
|
-
);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
});
|
|
1328
|
-
|
|
1329
|
-
// src/sources/footywire.ts
|
|
1330
|
-
import * as cheerio2 from "cheerio";
|
|
1331
|
-
function parseMatchList(html, year) {
|
|
1332
|
-
const $ = cheerio2.load(html);
|
|
1333
|
-
const results = [];
|
|
1334
|
-
let currentRound = 0;
|
|
1335
|
-
let currentRoundType = "HomeAndAway";
|
|
1336
|
-
$("tr").each((_i, row) => {
|
|
1337
|
-
const roundHeader = $(row).find("td[colspan='7']");
|
|
1338
|
-
if (roundHeader.length > 0) {
|
|
1339
|
-
const text = roundHeader.text().trim();
|
|
1340
|
-
currentRoundType = inferRoundType(text);
|
|
1341
|
-
const roundMatch = /Round\s+(\d+)/i.exec(text);
|
|
1342
|
-
if (roundMatch?.[1]) {
|
|
1343
|
-
currentRound = Number.parseInt(roundMatch[1], 10);
|
|
1344
|
-
}
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
const cells = $(row).find("td.data");
|
|
1348
|
-
if (cells.length < 5) return;
|
|
1349
|
-
const dateText = $(cells[0]).text().trim();
|
|
1350
|
-
const teamsCell = $(cells[1]);
|
|
1351
|
-
const venue = $(cells[2]).text().trim();
|
|
1352
|
-
const attendance = $(cells[3]).text().trim();
|
|
1353
|
-
const scoreCell = $(cells[4]);
|
|
1354
|
-
if (venue === "BYE") return;
|
|
1355
|
-
const teamLinks = teamsCell.find("a");
|
|
1356
|
-
if (teamLinks.length < 2) return;
|
|
1357
|
-
const homeTeam = normaliseTeamName($(teamLinks[0]).text().trim());
|
|
1358
|
-
const awayTeam = normaliseTeamName($(teamLinks[1]).text().trim());
|
|
1359
|
-
const scoreText = scoreCell.text().trim();
|
|
1360
|
-
const scoreMatch = /(\d+)-(\d+)/.exec(scoreText);
|
|
1361
|
-
if (!scoreMatch) return;
|
|
1362
|
-
const homePoints = Number.parseInt(scoreMatch[1] ?? "0", 10);
|
|
1363
|
-
const awayPoints = Number.parseInt(scoreMatch[2] ?? "0", 10);
|
|
1364
|
-
const scoreLink = scoreCell.find("a").attr("href") ?? "";
|
|
1365
|
-
const midMatch = /mid=(\d+)/.exec(scoreLink);
|
|
1366
|
-
const matchId = midMatch?.[1] ? `FW_${midMatch[1]}` : `FW_${year}_R${currentRound}_${homeTeam}`;
|
|
1367
|
-
const date = parseFootyWireDate(dateText) ?? new Date(year, 0, 1);
|
|
1368
|
-
const homeGoals = Math.floor(homePoints / 6);
|
|
1369
|
-
const homeBehinds = homePoints - homeGoals * 6;
|
|
1370
|
-
const awayGoals = Math.floor(awayPoints / 6);
|
|
1371
|
-
const awayBehinds = awayPoints - awayGoals * 6;
|
|
1372
|
-
results.push({
|
|
1373
|
-
matchId,
|
|
1374
|
-
season: year,
|
|
1375
|
-
roundNumber: currentRound,
|
|
1376
|
-
roundType: currentRoundType,
|
|
1377
|
-
date,
|
|
1378
|
-
venue,
|
|
1379
|
-
homeTeam,
|
|
1380
|
-
awayTeam,
|
|
1381
|
-
homeGoals,
|
|
1382
|
-
homeBehinds,
|
|
1383
|
-
homePoints,
|
|
1384
|
-
awayGoals,
|
|
1385
|
-
awayBehinds,
|
|
1386
|
-
awayPoints,
|
|
1387
|
-
margin: homePoints - awayPoints,
|
|
1388
|
-
q1Home: null,
|
|
1389
|
-
q2Home: null,
|
|
1390
|
-
q3Home: null,
|
|
1391
|
-
q4Home: null,
|
|
1392
|
-
q1Away: null,
|
|
1393
|
-
q2Away: null,
|
|
1394
|
-
q3Away: null,
|
|
1395
|
-
q4Away: null,
|
|
1396
|
-
status: "Complete",
|
|
1397
|
-
attendance: attendance ? Number.parseInt(attendance, 10) || null : null,
|
|
1398
|
-
venueState: null,
|
|
1399
|
-
venueTimezone: null,
|
|
1400
|
-
homeRushedBehinds: null,
|
|
1401
|
-
awayRushedBehinds: null,
|
|
1402
|
-
homeMinutesInFront: null,
|
|
1403
|
-
awayMinutesInFront: null,
|
|
1404
|
-
source: "footywire",
|
|
1405
|
-
competition: "AFLM"
|
|
1406
|
-
});
|
|
1407
|
-
});
|
|
1408
|
-
return results;
|
|
1409
|
-
}
|
|
1410
|
-
var FOOTYWIRE_BASE, FootyWireClient;
|
|
1411
|
-
var init_footywire = __esm({
|
|
1412
|
-
"src/sources/footywire.ts"() {
|
|
1413
|
-
"use strict";
|
|
1414
|
-
init_date_utils();
|
|
1415
|
-
init_errors();
|
|
1416
|
-
init_result();
|
|
1417
|
-
init_team_mapping();
|
|
1418
|
-
init_match_results();
|
|
1419
|
-
FOOTYWIRE_BASE = "https://www.footywire.com/afl/footy";
|
|
1420
|
-
FootyWireClient = class {
|
|
1421
|
-
fetchFn;
|
|
1422
|
-
constructor(options) {
|
|
1423
|
-
this.fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
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
|
+
}
|
|
1424
2621
|
}
|
|
1425
2622
|
/**
|
|
1426
|
-
* Fetch
|
|
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.
|
|
1427
2628
|
*/
|
|
1428
|
-
async
|
|
2629
|
+
async fetchSeasonPlayerStats(year) {
|
|
2630
|
+
const seasonUrl = `${AFL_TABLES_BASE}/${year}.html`;
|
|
1429
2631
|
try {
|
|
1430
|
-
const
|
|
1431
|
-
headers: {
|
|
1432
|
-
|
|
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);
|
|
1433
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" }
|
|
1434
2701
|
});
|
|
1435
2702
|
if (!response.ok) {
|
|
1436
2703
|
return err(
|
|
1437
|
-
new ScrapeError(
|
|
2704
|
+
new ScrapeError(
|
|
2705
|
+
`AFL Tables stats request failed: ${response.status} (${url})`,
|
|
2706
|
+
"afl-tables"
|
|
2707
|
+
)
|
|
1438
2708
|
);
|
|
1439
2709
|
}
|
|
1440
2710
|
const html = await response.text();
|
|
1441
|
-
|
|
2711
|
+
const entries = parseAflTablesTeamStats(html, year);
|
|
2712
|
+
return ok(entries);
|
|
1442
2713
|
} catch (cause) {
|
|
1443
2714
|
return err(
|
|
1444
2715
|
new ScrapeError(
|
|
1445
|
-
`
|
|
1446
|
-
"
|
|
2716
|
+
`AFL Tables team stats failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
2717
|
+
"afl-tables"
|
|
1447
2718
|
)
|
|
1448
2719
|
);
|
|
1449
2720
|
}
|
|
1450
2721
|
}
|
|
1451
2722
|
/**
|
|
1452
|
-
* Fetch
|
|
2723
|
+
* Fetch player list from AFL Tables team page.
|
|
1453
2724
|
*
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
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).
|
|
1456
2730
|
*/
|
|
1457
|
-
async
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
return htmlResult;
|
|
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"));
|
|
1462
2735
|
}
|
|
2736
|
+
const url = `https://afltables.com/afl/stats/alltime/${slug}.html`;
|
|
1463
2737
|
try {
|
|
1464
|
-
const
|
|
1465
|
-
|
|
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);
|
|
1466
2749
|
} catch (cause) {
|
|
1467
2750
|
return err(
|
|
1468
2751
|
new ScrapeError(
|
|
1469
|
-
`
|
|
1470
|
-
"
|
|
2752
|
+
`AFL Tables player list failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
2753
|
+
"afl-tables"
|
|
1471
2754
|
)
|
|
1472
2755
|
);
|
|
1473
2756
|
}
|
|
1474
2757
|
}
|
|
1475
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();
|
|
1476
3035
|
}
|
|
1477
3036
|
});
|
|
1478
3037
|
|
|
@@ -1514,6 +3073,12 @@ async function fetchMatchResults(query) {
|
|
|
1514
3073
|
}
|
|
1515
3074
|
return result;
|
|
1516
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
|
+
}
|
|
1517
3082
|
default:
|
|
1518
3083
|
return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
|
|
1519
3084
|
}
|
|
@@ -1526,7 +3091,116 @@ var init_match_results2 = __esm({
|
|
|
1526
3091
|
init_afl_api();
|
|
1527
3092
|
init_afl_tables();
|
|
1528
3093
|
init_footywire();
|
|
3094
|
+
init_squiggle();
|
|
1529
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();
|
|
1530
3204
|
}
|
|
1531
3205
|
});
|
|
1532
3206
|
|
|
@@ -1685,20 +3359,44 @@ async function fetchPlayerStats(query) {
|
|
|
1685
3359
|
}
|
|
1686
3360
|
return ok(allStats);
|
|
1687
3361
|
}
|
|
1688
|
-
case "footywire":
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
+
}
|
|
1702
3400
|
default:
|
|
1703
3401
|
return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
|
|
1704
3402
|
}
|
|
@@ -1709,28 +3407,63 @@ var init_player_stats2 = __esm({
|
|
|
1709
3407
|
init_errors();
|
|
1710
3408
|
init_result();
|
|
1711
3409
|
init_afl_api();
|
|
3410
|
+
init_afl_tables();
|
|
3411
|
+
init_footywire();
|
|
1712
3412
|
init_player_stats();
|
|
1713
3413
|
}
|
|
1714
3414
|
});
|
|
1715
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
|
+
|
|
1716
3450
|
// src/api/teams.ts
|
|
1717
3451
|
function teamTypeForComp(comp) {
|
|
1718
3452
|
return comp === "AFLW" ? "WOMEN" : "MEN";
|
|
1719
3453
|
}
|
|
1720
3454
|
async function fetchTeams(query) {
|
|
1721
3455
|
const client = new AflApiClient();
|
|
1722
|
-
const teamType = query?.teamType ?? (query?.competition
|
|
3456
|
+
const teamType = query?.teamType ?? teamTypeForComp(query?.competition ?? "AFLM");
|
|
1723
3457
|
const result = await client.fetchTeams(teamType);
|
|
1724
3458
|
if (!result.success) return result;
|
|
1725
3459
|
const competition = query?.competition ?? "AFLM";
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
);
|
|
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);
|
|
1734
3467
|
}
|
|
1735
3468
|
async function fetchSquad(query) {
|
|
1736
3469
|
const client = new AflApiClient();
|
|
@@ -1781,11 +3514,14 @@ var init_teams = __esm({
|
|
|
1781
3514
|
var init_index = __esm({
|
|
1782
3515
|
"src/index.ts"() {
|
|
1783
3516
|
"use strict";
|
|
3517
|
+
init_coaches_votes();
|
|
1784
3518
|
init_fixture();
|
|
1785
3519
|
init_ladder2();
|
|
1786
3520
|
init_lineup2();
|
|
1787
3521
|
init_match_results2();
|
|
3522
|
+
init_player_details();
|
|
1788
3523
|
init_player_stats2();
|
|
3524
|
+
init_team_stats();
|
|
1789
3525
|
init_teams();
|
|
1790
3526
|
}
|
|
1791
3527
|
});
|
|
@@ -2462,14 +4198,232 @@ var init_teams2 = __esm({
|
|
|
2462
4198
|
}
|
|
2463
4199
|
});
|
|
2464
4200
|
|
|
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
|
+
});
|
|
4212
|
+
}
|
|
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
|
+
});
|
|
4269
|
+
}
|
|
4270
|
+
});
|
|
4271
|
+
|
|
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
|
+
});
|
|
4344
|
+
}
|
|
4345
|
+
});
|
|
4346
|
+
|
|
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
|
+
});
|
|
4416
|
+
}
|
|
4417
|
+
});
|
|
4418
|
+
|
|
2465
4419
|
// src/cli.ts
|
|
2466
4420
|
init_errors();
|
|
2467
|
-
import { defineCommand as
|
|
4421
|
+
import { defineCommand as defineCommand11, runMain } from "citty";
|
|
2468
4422
|
import pc2 from "picocolors";
|
|
2469
|
-
var main =
|
|
4423
|
+
var main = defineCommand11({
|
|
2470
4424
|
meta: {
|
|
2471
4425
|
name: "fitzroy",
|
|
2472
|
-
version: "1.0
|
|
4426
|
+
version: "1.1.0",
|
|
2473
4427
|
description: "CLI for fetching AFL data \u2014 match results, player stats, fixtures, ladders, and more"
|
|
2474
4428
|
},
|
|
2475
4429
|
subCommands: {
|
|
@@ -2479,7 +4433,10 @@ var main = defineCommand8({
|
|
|
2479
4433
|
ladder: () => Promise.resolve().then(() => (init_ladder3(), ladder_exports)).then((m) => m.ladderCommand),
|
|
2480
4434
|
lineup: () => Promise.resolve().then(() => (init_lineup3(), lineup_exports)).then((m) => m.lineupCommand),
|
|
2481
4435
|
squad: () => Promise.resolve().then(() => (init_squad(), squad_exports)).then((m) => m.squadCommand),
|
|
2482
|
-
teams: () => Promise.resolve().then(() => (init_teams2(), teams_exports)).then((m) => m.teamsCommand)
|
|
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)
|
|
2483
4440
|
}
|
|
2484
4441
|
});
|
|
2485
4442
|
function formatError(error) {
|