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.
Files changed (4) hide show
  1. package/dist/cli.js +2630 -673
  2. package/dist/index.d.ts +1372 -914
  3. package/dist/index.js +2874 -1056
  4. 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/validation.ts
123
- import { z } from "zod/v4";
124
- 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;
125
- var init_validation = __esm({
126
- "src/lib/validation.ts"() {
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
- AflApiTokenSchema = z.object({
129
- token: z.string(),
130
- disclaimer: z.string().optional()
131
- }).passthrough();
132
- CompetitionSchema = z.object({
133
- id: z.number(),
134
- name: z.string(),
135
- code: z.string().optional()
136
- }).passthrough();
137
- CompetitionListSchema = z.object({
138
- competitions: z.array(CompetitionSchema)
139
- }).passthrough();
140
- CompseasonSchema = z.object({
141
- id: z.number(),
142
- name: z.string(),
143
- shortName: z.string().optional(),
144
- currentRoundNumber: z.number().optional()
145
- }).passthrough();
146
- CompseasonListSchema = z.object({
147
- compSeasons: z.array(CompseasonSchema)
148
- }).passthrough();
149
- RoundSchema = z.object({
150
- id: z.number(),
151
- /** Provider ID used by /cfs/ endpoints (e.g. "CD_R202501401"). */
152
- providerId: z.string().optional(),
153
- name: z.string(),
154
- abbreviation: z.string().optional(),
155
- roundNumber: z.number(),
156
- utcStartTime: z.string().optional(),
157
- utcEndTime: z.string().optional()
158
- }).passthrough();
159
- RoundListSchema = z.object({
160
- rounds: z.array(RoundSchema)
161
- }).passthrough();
162
- ScoreSchema = z.object({
163
- totalScore: z.number(),
164
- goals: z.number(),
165
- behinds: z.number(),
166
- superGoals: z.number().nullable().optional()
167
- }).passthrough();
168
- PeriodScoreSchema = z.object({
169
- periodNumber: z.number(),
170
- score: ScoreSchema
171
- }).passthrough();
172
- TeamScoreSchema = z.object({
173
- matchScore: ScoreSchema,
174
- periodScore: z.array(PeriodScoreSchema).optional(),
175
- rushedBehinds: z.number().optional(),
176
- minutesInFront: z.number().optional()
177
- }).passthrough();
178
- CfsMatchTeamSchema = z.object({
179
- name: z.string(),
180
- teamId: z.string(),
181
- abbr: z.string().optional(),
182
- nickname: z.string().optional()
183
- }).passthrough();
184
- CfsMatchSchema = z.object({
185
- matchId: z.string(),
186
- name: z.string().optional(),
187
- status: z.string(),
188
- utcStartTime: z.string(),
189
- homeTeamId: z.string(),
190
- awayTeamId: z.string(),
191
- homeTeam: CfsMatchTeamSchema,
192
- awayTeam: CfsMatchTeamSchema,
193
- round: z.string().optional(),
194
- abbr: z.string().optional()
195
- }).passthrough();
196
- CfsScoreSchema = z.object({
197
- status: z.string(),
198
- matchId: z.string(),
199
- homeTeamScore: TeamScoreSchema,
200
- awayTeamScore: TeamScoreSchema
201
- }).passthrough();
202
- CfsVenueSchema = z.object({
203
- name: z.string(),
204
- venueId: z.string().optional(),
205
- state: z.string().optional(),
206
- timeZone: z.string().optional()
207
- }).passthrough();
208
- MatchItemSchema = z.object({
209
- match: CfsMatchSchema,
210
- score: CfsScoreSchema.optional(),
211
- venue: CfsVenueSchema.optional(),
212
- round: z.object({
213
- name: z.string(),
214
- roundId: z.string(),
215
- roundNumber: z.number()
216
- }).passthrough().optional()
217
- }).passthrough();
218
- MatchItemListSchema = z.object({
219
- roundId: z.string().optional(),
220
- items: z.array(MatchItemSchema)
221
- }).passthrough();
222
- CfsPlayerInnerSchema = z.object({
223
- playerId: z.string(),
224
- playerName: z.object({
225
- givenName: z.string(),
226
- surname: z.string()
227
- }).passthrough(),
228
- captain: z.boolean().optional(),
229
- playerJumperNumber: z.number().optional()
230
- }).passthrough();
231
- PlayerGameStatsSchema = z.object({
232
- goals: z.number().optional(),
233
- behinds: z.number().optional(),
234
- kicks: z.number().optional(),
235
- handballs: z.number().optional(),
236
- disposals: z.number().optional(),
237
- marks: z.number().optional(),
238
- bounces: z.number().optional(),
239
- tackles: z.number().optional(),
240
- contestedPossessions: z.number().optional(),
241
- uncontestedPossessions: z.number().optional(),
242
- totalPossessions: z.number().optional(),
243
- inside50s: z.number().optional(),
244
- marksInside50: z.number().optional(),
245
- contestedMarks: z.number().optional(),
246
- hitouts: z.number().optional(),
247
- onePercenters: z.number().optional(),
248
- disposalEfficiency: z.number().optional(),
249
- clangers: z.number().optional(),
250
- freesFor: z.number().optional(),
251
- freesAgainst: z.number().optional(),
252
- dreamTeamPoints: z.number().optional(),
253
- clearances: z.object({
254
- centreClearances: z.number().optional(),
255
- stoppageClearances: z.number().optional(),
256
- totalClearances: z.number().optional()
257
- }).passthrough().optional(),
258
- rebound50s: z.number().optional(),
259
- goalAssists: z.number().optional(),
260
- goalAccuracy: z.number().optional(),
261
- turnovers: z.number().optional(),
262
- intercepts: z.number().optional(),
263
- tacklesInside50: z.number().optional(),
264
- shotsAtGoal: z.number().optional(),
265
- metresGained: z.number().optional(),
266
- scoreInvolvements: z.number().optional(),
267
- ratingPoints: z.number().optional(),
268
- extendedStats: z.object({
269
- effectiveDisposals: z.number().optional(),
270
- effectiveKicks: z.number().optional(),
271
- kickEfficiency: z.number().optional(),
272
- kickToHandballRatio: z.number().optional(),
273
- pressureActs: z.number().optional(),
274
- defHalfPressureActs: z.number().optional(),
275
- spoils: z.number().optional(),
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/match-results.ts
765
- function inferRoundType(roundName) {
766
- return FINALS_PATTERN.test(roundName) ? "Finals" : "HomeAndAway";
767
- }
768
- function toMatchStatus(raw) {
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 toQuarterScore(period) {
788
- return {
789
- goals: period.score.goals,
790
- behinds: period.score.behinds,
791
- points: period.score.totalScore
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 findPeriod(periods, quarter) {
795
- if (!periods) return null;
796
- const period = periods.find((p) => p.periodNumber === quarter);
797
- return period ? toQuarterScore(period) : null;
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 transformMatchItems(items, season, competition, source = "afl-api") {
800
- return items.map((item) => {
801
- const homeScore = item.score?.homeTeamScore;
802
- const awayScore = item.score?.awayTeamScore;
803
- const homePoints = homeScore?.matchScore.totalScore ?? 0;
804
- const awayPoints = awayScore?.matchScore.totalScore ?? 0;
805
- return {
806
- matchId: item.match.matchId,
807
- season,
808
- roundNumber: item.round?.roundNumber ?? 0,
809
- roundType: inferRoundType(item.round?.name ?? ""),
810
- date: new Date(item.match.utcStartTime),
811
- venue: item.venue?.name ?? "",
812
- homeTeam: normaliseTeamName(item.match.homeTeam.name),
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 FINALS_PATTERN;
843
- var init_match_results = __esm({
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
- FINALS_PATTERN = /final|elimination|qualifying|preliminary|semi|grand/i;
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 source.",
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(lineups);
2224
+ return ok(fixtures);
1059
2225
  }
1060
- var init_lineup2 = __esm({
1061
- "src/api/lineup.ts"() {
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
- init_lineup();
2233
+ init_footywire();
2234
+ init_squiggle();
2235
+ init_match_results();
2236
+ init_squiggle2();
1067
2237
  }
1068
2238
  });
1069
2239
 
1070
- // src/lib/date-utils.ts
1071
- function parseFootyWireDate(dateStr) {
1072
- const trimmed = dateStr.trim();
1073
- if (trimmed === "") {
1074
- return null;
1075
- }
1076
- const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
1077
- const normalised = withoutDow.replace(/-/g, " ");
1078
- const match = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
1079
- if (!match) {
1080
- return null;
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 parseAflTablesDate(dateStr) {
1102
- const trimmed = dateStr.trim();
1103
- if (trimmed === "") {
1104
- return null;
1105
- }
1106
- const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
1107
- const normalised = withoutDow.replace(/[-/]/g, " ");
1108
- const dmy = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
1109
- if (dmy) {
1110
- const [, dayStr, monthStr, yearStr] = dmy;
1111
- if (dayStr && monthStr && yearStr) {
1112
- return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
1113
- }
1114
- }
1115
- const mdy = /^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})$/.exec(normalised);
1116
- if (mdy) {
1117
- const [, monthStr, dayStr, yearStr] = mdy;
1118
- if (dayStr && monthStr && yearStr) {
1119
- return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
1120
- }
1121
- }
1122
- return null;
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 buildUtcDate(year, monthStr, day) {
1125
- const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
1126
- if (monthIndex === void 0) {
1127
- return null;
1128
- }
1129
- const date = new Date(Date.UTC(year, monthIndex, day));
1130
- if (Number.isNaN(date.getTime())) {
1131
- return null;
1132
- }
1133
- if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
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 MONTH_ABBREV_TO_INDEX;
1139
- var init_date_utils = __esm({
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
- MONTH_ABBREV_TO_INDEX = /* @__PURE__ */ new Map([
1143
- ["jan", 0],
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 cheerio from "cheerio";
2365
+ import * as cheerio5 from "cheerio";
1172
2366
  function parseSeasonPage(html, year) {
1173
- const $ = cheerio.load(html);
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] && !$table.attr("border")) {
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 (!$table.attr("border") && inferRoundType(text) === "Finals") {
2382
+ if (border !== "1" && inferRoundType(text) === "Finals") {
1188
2383
  currentRoundType = "Finals";
1189
2384
  return;
1190
2385
  }
1191
- if ($table.attr("border") !== "1") return;
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 $ = cheerio.load(html);
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
- var AFL_TABLES_BASE, AflTablesClient;
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 the HTML content of a FootyWire page.
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 fetchHtml(url) {
2629
+ async fetchSeasonPlayerStats(year) {
2630
+ const seasonUrl = `${AFL_TABLES_BASE}/${year}.html`;
1429
2631
  try {
1430
- const response = await this.fetchFn(url, {
1431
- headers: {
1432
- "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"
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(`FootyWire request failed: ${response.status} (${url})`, "footywire")
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
- return ok(html);
2711
+ const entries = parseAflTablesTeamStats(html, year);
2712
+ return ok(entries);
1442
2713
  } catch (cause) {
1443
2714
  return err(
1444
2715
  new ScrapeError(
1445
- `FootyWire request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
1446
- "footywire"
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 season match results from FootyWire.
2723
+ * Fetch player list from AFL Tables team page.
1453
2724
  *
1454
- * @param year - The season year.
1455
- * @returns Array of match results.
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 fetchSeasonResults(year) {
1458
- const url = `${FOOTYWIRE_BASE}/ft_match_list?year=${year}`;
1459
- const htmlResult = await this.fetchHtml(url);
1460
- if (!htmlResult.success) {
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 results = parseMatchList(htmlResult.data, year);
1465
- return ok(results);
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
- `Failed to parse match list: ${cause instanceof Error ? cause.message : String(cause)}`,
1470
- "footywire"
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
- return err(
1690
- new UnsupportedSourceError(
1691
- "Player stats from FootyWire are not yet supported. Use source: 'afl-api'.",
1692
- "footywire"
1693
- )
1694
- );
1695
- case "afl-tables":
1696
- return err(
1697
- new UnsupportedSourceError(
1698
- "Player stats from AFL Tables are not yet supported. Use source: 'afl-api'.",
1699
- "afl-tables"
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 ? teamTypeForComp(query.competition) : void 0);
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
- return ok(
1727
- result.data.map((t) => ({
1728
- teamId: String(t.id),
1729
- name: normaliseTeamName(t.name),
1730
- abbreviation: t.abbreviation ?? "",
1731
- competition
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 defineCommand8, runMain } from "citty";
4421
+ import { defineCommand as defineCommand11, runMain } from "citty";
2468
4422
  import pc2 from "picocolors";
2469
- var main = defineCommand8({
4423
+ var main = defineCommand11({
2470
4424
  meta: {
2471
4425
  name: "fitzroy",
2472
- version: "1.0.2",
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) {