fitzroy 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2470 -50
- package/dist/index.js +1658 -61
- package/package.json +3 -2
- package/dist/shared/chunk-99nkfy8s.js +0 -65
- package/dist/shared/chunk-9zcjfgwe.js +0 -66
- package/dist/shared/chunk-b380x0p6.js +0 -54
- package/dist/shared/chunk-c7vawngt.js +0 -63
- package/dist/shared/chunk-d6fkap72.js +0 -67
- package/dist/shared/chunk-eyrvakjt.js +0 -1
- package/dist/shared/chunk-kr78ch1j.js +0 -67
- package/dist/shared/chunk-ngvkaczn.js +0 -70
- package/dist/shared/chunk-xv8z2kms.js +0 -4
- package/dist/shared/chunk-z78xs4nr.js +0 -185
package/dist/index.js
CHANGED
|
@@ -1,4 +1,1661 @@
|
|
|
1
|
-
|
|
1
|
+
// src/lib/errors.ts
|
|
2
|
+
var AflApiError = class extends Error {
|
|
3
|
+
constructor(message, statusCode) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.statusCode = statusCode;
|
|
6
|
+
}
|
|
7
|
+
name = "AflApiError";
|
|
8
|
+
};
|
|
9
|
+
var ScrapeError = class extends Error {
|
|
10
|
+
constructor(message, source) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.source = source;
|
|
13
|
+
}
|
|
14
|
+
name = "ScrapeError";
|
|
15
|
+
};
|
|
16
|
+
var UnsupportedSourceError = class extends Error {
|
|
17
|
+
constructor(message, source) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.source = source;
|
|
20
|
+
}
|
|
21
|
+
name = "UnsupportedSourceError";
|
|
22
|
+
};
|
|
23
|
+
var ValidationError = class extends Error {
|
|
24
|
+
constructor(message, issues) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.issues = issues;
|
|
27
|
+
}
|
|
28
|
+
name = "ValidationError";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/lib/result.ts
|
|
32
|
+
function ok(data) {
|
|
33
|
+
return { success: true, data };
|
|
34
|
+
}
|
|
35
|
+
function err(error) {
|
|
36
|
+
return { success: false, error };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/lib/team-mapping.ts
|
|
40
|
+
var TEAM_ALIASES = [
|
|
41
|
+
["Adelaide Crows", "Adelaide", "Crows", "ADEL", "AD"],
|
|
42
|
+
["Brisbane Lions", "Brisbane", "Brisbane Bears", "Bears", "Fitzroy Lions", "BL", "BRIS"],
|
|
43
|
+
["Carlton", "Carlton Blues", "Blues", "CARL", "CA"],
|
|
44
|
+
["Collingwood", "Collingwood Magpies", "Magpies", "COLL", "CW"],
|
|
45
|
+
["Essendon", "Essendon Bombers", "Bombers", "ESS", "ES"],
|
|
46
|
+
["Fremantle", "Fremantle Dockers", "Dockers", "FRE", "FR"],
|
|
47
|
+
["Geelong Cats", "Geelong", "Cats", "GEEL", "GE"],
|
|
48
|
+
[
|
|
49
|
+
"Gold Coast Suns",
|
|
50
|
+
"Gold Coast",
|
|
51
|
+
"Gold Coast SUNS",
|
|
52
|
+
"Gold Coast Football Club",
|
|
53
|
+
"Suns",
|
|
54
|
+
"GCFC",
|
|
55
|
+
"GC"
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"GWS Giants",
|
|
59
|
+
"GWS",
|
|
60
|
+
"GWS GIANTS",
|
|
61
|
+
"Greater Western Sydney",
|
|
62
|
+
"Giants",
|
|
63
|
+
"Greater Western Sydney Giants",
|
|
64
|
+
"GW"
|
|
65
|
+
],
|
|
66
|
+
["Hawthorn", "Hawthorn Hawks", "Hawks", "HAW", "HW"],
|
|
67
|
+
["Melbourne", "Melbourne Demons", "Demons", "MELB", "ME"],
|
|
68
|
+
["North Melbourne", "North Melbourne Kangaroos", "Kangaroos", "Kangas", "North", "NMFC", "NM"],
|
|
69
|
+
["Port Adelaide", "Port Adelaide Power", "Power", "Port", "PA", "PAFC"],
|
|
70
|
+
["Richmond", "Richmond Tigers", "Tigers", "RICH", "RI"],
|
|
71
|
+
["St Kilda", "St Kilda Saints", "Saints", "Saint Kilda", "STK", "SK"],
|
|
72
|
+
["Sydney Swans", "Sydney", "Swans", "South Melbourne", "South Melbourne Swans", "SYD", "SY"],
|
|
73
|
+
["West Coast Eagles", "West Coast", "Eagles", "WCE", "WC"],
|
|
74
|
+
["Western Bulldogs", "Bulldogs", "Footscray", "Footscray Bulldogs", "WB", "WBD"],
|
|
75
|
+
// Historical / defunct VFL teams
|
|
76
|
+
["Fitzroy", "Fitzroy Reds", "Fitzroy Gorillas", "Fitzroy Maroons", "FI"],
|
|
77
|
+
["University", "University Blacks"]
|
|
78
|
+
];
|
|
79
|
+
var ALIAS_MAP = (() => {
|
|
80
|
+
const map = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const [canonical, ...aliases] of TEAM_ALIASES) {
|
|
82
|
+
map.set(canonical.toLowerCase(), canonical);
|
|
83
|
+
for (const alias of aliases) {
|
|
84
|
+
map.set(alias.toLowerCase(), canonical);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return map;
|
|
88
|
+
})();
|
|
89
|
+
function normaliseTeamName(raw) {
|
|
90
|
+
const trimmed = raw.trim();
|
|
91
|
+
return ALIAS_MAP.get(trimmed.toLowerCase()) ?? trimmed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/lib/validation.ts
|
|
95
|
+
import { z } from "zod/v4";
|
|
96
|
+
var AflApiTokenSchema = z.object({
|
|
97
|
+
token: z.string(),
|
|
98
|
+
disclaimer: z.string().optional()
|
|
99
|
+
}).passthrough();
|
|
100
|
+
var CompetitionSchema = z.object({
|
|
101
|
+
id: z.number(),
|
|
102
|
+
name: z.string(),
|
|
103
|
+
code: z.string().optional()
|
|
104
|
+
}).passthrough();
|
|
105
|
+
var CompetitionListSchema = z.object({
|
|
106
|
+
competitions: z.array(CompetitionSchema)
|
|
107
|
+
}).passthrough();
|
|
108
|
+
var CompseasonSchema = z.object({
|
|
109
|
+
id: z.number(),
|
|
110
|
+
name: z.string(),
|
|
111
|
+
shortName: z.string().optional(),
|
|
112
|
+
currentRoundNumber: z.number().optional()
|
|
113
|
+
}).passthrough();
|
|
114
|
+
var CompseasonListSchema = z.object({
|
|
115
|
+
compSeasons: z.array(CompseasonSchema)
|
|
116
|
+
}).passthrough();
|
|
117
|
+
var RoundSchema = z.object({
|
|
118
|
+
id: z.number(),
|
|
119
|
+
/** Provider ID used by /cfs/ endpoints (e.g. "CD_R202501401"). */
|
|
120
|
+
providerId: z.string().optional(),
|
|
121
|
+
name: z.string(),
|
|
122
|
+
abbreviation: z.string().optional(),
|
|
123
|
+
roundNumber: z.number(),
|
|
124
|
+
utcStartTime: z.string().optional(),
|
|
125
|
+
utcEndTime: z.string().optional()
|
|
126
|
+
}).passthrough();
|
|
127
|
+
var RoundListSchema = z.object({
|
|
128
|
+
rounds: z.array(RoundSchema)
|
|
129
|
+
}).passthrough();
|
|
130
|
+
var ScoreSchema = z.object({
|
|
131
|
+
totalScore: z.number(),
|
|
132
|
+
goals: z.number(),
|
|
133
|
+
behinds: z.number(),
|
|
134
|
+
superGoals: z.number().nullable().optional()
|
|
135
|
+
}).passthrough();
|
|
136
|
+
var PeriodScoreSchema = z.object({
|
|
137
|
+
periodNumber: z.number(),
|
|
138
|
+
score: ScoreSchema
|
|
139
|
+
}).passthrough();
|
|
140
|
+
var TeamScoreSchema = z.object({
|
|
141
|
+
matchScore: ScoreSchema,
|
|
142
|
+
periodScore: z.array(PeriodScoreSchema).optional(),
|
|
143
|
+
rushedBehinds: z.number().optional(),
|
|
144
|
+
minutesInFront: z.number().optional()
|
|
145
|
+
}).passthrough();
|
|
146
|
+
var CfsMatchTeamSchema = z.object({
|
|
147
|
+
name: z.string(),
|
|
148
|
+
teamId: z.string(),
|
|
149
|
+
abbr: z.string().optional(),
|
|
150
|
+
nickname: z.string().optional()
|
|
151
|
+
}).passthrough();
|
|
152
|
+
var CfsMatchSchema = z.object({
|
|
153
|
+
matchId: z.string(),
|
|
154
|
+
name: z.string().optional(),
|
|
155
|
+
status: z.string(),
|
|
156
|
+
utcStartTime: z.string(),
|
|
157
|
+
homeTeamId: z.string(),
|
|
158
|
+
awayTeamId: z.string(),
|
|
159
|
+
homeTeam: CfsMatchTeamSchema,
|
|
160
|
+
awayTeam: CfsMatchTeamSchema,
|
|
161
|
+
round: z.string().optional(),
|
|
162
|
+
abbr: z.string().optional()
|
|
163
|
+
}).passthrough();
|
|
164
|
+
var CfsScoreSchema = z.object({
|
|
165
|
+
status: z.string(),
|
|
166
|
+
matchId: z.string(),
|
|
167
|
+
homeTeamScore: TeamScoreSchema,
|
|
168
|
+
awayTeamScore: TeamScoreSchema
|
|
169
|
+
}).passthrough();
|
|
170
|
+
var CfsVenueSchema = z.object({
|
|
171
|
+
name: z.string(),
|
|
172
|
+
venueId: z.string().optional(),
|
|
173
|
+
state: z.string().optional(),
|
|
174
|
+
timeZone: z.string().optional()
|
|
175
|
+
}).passthrough();
|
|
176
|
+
var MatchItemSchema = z.object({
|
|
177
|
+
match: CfsMatchSchema,
|
|
178
|
+
score: CfsScoreSchema.optional(),
|
|
179
|
+
venue: CfsVenueSchema.optional(),
|
|
180
|
+
round: z.object({
|
|
181
|
+
name: z.string(),
|
|
182
|
+
roundId: z.string(),
|
|
183
|
+
roundNumber: z.number()
|
|
184
|
+
}).passthrough().optional()
|
|
185
|
+
}).passthrough();
|
|
186
|
+
var MatchItemListSchema = z.object({
|
|
187
|
+
roundId: z.string().optional(),
|
|
188
|
+
items: z.array(MatchItemSchema)
|
|
189
|
+
}).passthrough();
|
|
190
|
+
var CfsPlayerInnerSchema = z.object({
|
|
191
|
+
playerId: z.string(),
|
|
192
|
+
playerName: z.object({
|
|
193
|
+
givenName: z.string(),
|
|
194
|
+
surname: z.string()
|
|
195
|
+
}).passthrough(),
|
|
196
|
+
captain: z.boolean().optional(),
|
|
197
|
+
playerJumperNumber: z.number().optional()
|
|
198
|
+
}).passthrough();
|
|
199
|
+
var PlayerGameStatsSchema = z.object({
|
|
200
|
+
goals: z.number().optional(),
|
|
201
|
+
behinds: z.number().optional(),
|
|
202
|
+
kicks: z.number().optional(),
|
|
203
|
+
handballs: z.number().optional(),
|
|
204
|
+
disposals: z.number().optional(),
|
|
205
|
+
marks: z.number().optional(),
|
|
206
|
+
bounces: z.number().optional(),
|
|
207
|
+
tackles: z.number().optional(),
|
|
208
|
+
contestedPossessions: z.number().optional(),
|
|
209
|
+
uncontestedPossessions: z.number().optional(),
|
|
210
|
+
totalPossessions: z.number().optional(),
|
|
211
|
+
inside50s: z.number().optional(),
|
|
212
|
+
marksInside50: z.number().optional(),
|
|
213
|
+
contestedMarks: z.number().optional(),
|
|
214
|
+
hitouts: z.number().optional(),
|
|
215
|
+
onePercenters: z.number().optional(),
|
|
216
|
+
disposalEfficiency: z.number().optional(),
|
|
217
|
+
clangers: z.number().optional(),
|
|
218
|
+
freesFor: z.number().optional(),
|
|
219
|
+
freesAgainst: z.number().optional(),
|
|
220
|
+
dreamTeamPoints: z.number().optional(),
|
|
221
|
+
clearances: z.object({
|
|
222
|
+
centreClearances: z.number().optional(),
|
|
223
|
+
stoppageClearances: z.number().optional(),
|
|
224
|
+
totalClearances: z.number().optional()
|
|
225
|
+
}).passthrough().optional(),
|
|
226
|
+
rebound50s: z.number().optional(),
|
|
227
|
+
goalAssists: z.number().optional(),
|
|
228
|
+
goalAccuracy: z.number().optional(),
|
|
229
|
+
turnovers: z.number().optional(),
|
|
230
|
+
intercepts: z.number().optional(),
|
|
231
|
+
tacklesInside50: z.number().optional(),
|
|
232
|
+
shotsAtGoal: z.number().optional(),
|
|
233
|
+
metresGained: z.number().optional(),
|
|
234
|
+
scoreInvolvements: z.number().optional(),
|
|
235
|
+
ratingPoints: z.number().optional(),
|
|
236
|
+
extendedStats: z.object({
|
|
237
|
+
effectiveDisposals: z.number().optional(),
|
|
238
|
+
effectiveKicks: z.number().optional(),
|
|
239
|
+
kickEfficiency: z.number().optional(),
|
|
240
|
+
kickToHandballRatio: z.number().optional(),
|
|
241
|
+
pressureActs: z.number().optional(),
|
|
242
|
+
defHalfPressureActs: z.number().optional(),
|
|
243
|
+
spoils: z.number().optional(),
|
|
244
|
+
hitoutsToAdvantage: z.number().optional(),
|
|
245
|
+
hitoutWinPercentage: z.number().optional(),
|
|
246
|
+
hitoutToAdvantageRate: z.number().optional(),
|
|
247
|
+
groundBallGets: z.number().optional(),
|
|
248
|
+
f50GroundBallGets: z.number().optional(),
|
|
249
|
+
interceptMarks: z.number().optional(),
|
|
250
|
+
marksOnLead: z.number().optional(),
|
|
251
|
+
contestedPossessionRate: z.number().optional(),
|
|
252
|
+
contestOffOneOnOnes: z.number().optional(),
|
|
253
|
+
contestOffWins: z.number().optional(),
|
|
254
|
+
contestOffWinsPercentage: z.number().optional(),
|
|
255
|
+
contestDefOneOnOnes: z.number().optional(),
|
|
256
|
+
contestDefLosses: z.number().optional(),
|
|
257
|
+
contestDefLossPercentage: z.number().optional(),
|
|
258
|
+
centreBounceAttendances: z.number().optional(),
|
|
259
|
+
kickins: z.number().optional(),
|
|
260
|
+
kickinsPlayon: z.number().optional(),
|
|
261
|
+
ruckContests: z.number().optional(),
|
|
262
|
+
scoreLaunches: z.number().optional()
|
|
263
|
+
}).passthrough().optional()
|
|
264
|
+
}).passthrough();
|
|
265
|
+
var PlayerStatsItemSchema = z.object({
|
|
266
|
+
player: z.object({
|
|
267
|
+
player: z.object({
|
|
268
|
+
position: z.string().optional(),
|
|
269
|
+
player: CfsPlayerInnerSchema
|
|
270
|
+
}).passthrough(),
|
|
271
|
+
jumperNumber: z.number().optional()
|
|
272
|
+
}).passthrough(),
|
|
273
|
+
teamId: z.string(),
|
|
274
|
+
playerStats: z.object({
|
|
275
|
+
stats: PlayerGameStatsSchema,
|
|
276
|
+
timeOnGroundPercentage: z.number().optional()
|
|
277
|
+
}).passthrough()
|
|
278
|
+
}).passthrough();
|
|
279
|
+
var PlayerStatsListSchema = z.object({
|
|
280
|
+
homeTeamPlayerStats: z.array(PlayerStatsItemSchema),
|
|
281
|
+
awayTeamPlayerStats: z.array(PlayerStatsItemSchema)
|
|
282
|
+
}).passthrough();
|
|
283
|
+
var RosterPlayerSchema = z.object({
|
|
284
|
+
player: z.object({
|
|
285
|
+
position: z.string().optional(),
|
|
286
|
+
player: CfsPlayerInnerSchema
|
|
287
|
+
}).passthrough(),
|
|
288
|
+
jumperNumber: z.number().optional()
|
|
289
|
+
}).passthrough();
|
|
290
|
+
var TeamPlayersSchema = z.object({
|
|
291
|
+
teamId: z.string(),
|
|
292
|
+
players: z.array(RosterPlayerSchema)
|
|
293
|
+
}).passthrough();
|
|
294
|
+
var MatchRosterSchema = z.object({
|
|
295
|
+
match: CfsMatchSchema,
|
|
296
|
+
teamPlayers: z.array(TeamPlayersSchema)
|
|
297
|
+
}).passthrough();
|
|
298
|
+
var TeamItemSchema = z.object({
|
|
299
|
+
id: z.number(),
|
|
300
|
+
name: z.string(),
|
|
301
|
+
abbreviation: z.string().optional(),
|
|
302
|
+
teamType: z.string().optional()
|
|
303
|
+
}).passthrough();
|
|
304
|
+
var TeamListSchema = z.object({
|
|
305
|
+
teams: z.array(TeamItemSchema)
|
|
306
|
+
}).passthrough();
|
|
307
|
+
var SquadPlayerInnerSchema = z.object({
|
|
308
|
+
id: z.number(),
|
|
309
|
+
providerId: z.string().optional(),
|
|
310
|
+
firstName: z.string(),
|
|
311
|
+
surname: z.string(),
|
|
312
|
+
dateOfBirth: z.string().optional(),
|
|
313
|
+
heightInCm: z.number().optional(),
|
|
314
|
+
weightInKg: z.number().optional(),
|
|
315
|
+
draftYear: z.string().optional(),
|
|
316
|
+
draftPosition: z.string().optional(),
|
|
317
|
+
draftType: z.string().optional(),
|
|
318
|
+
debutYear: z.string().optional(),
|
|
319
|
+
recruitedFrom: z.string().optional()
|
|
320
|
+
}).passthrough();
|
|
321
|
+
var SquadPlayerItemSchema = z.object({
|
|
322
|
+
player: SquadPlayerInnerSchema,
|
|
323
|
+
jumperNumber: z.number().optional(),
|
|
324
|
+
position: z.string().optional()
|
|
325
|
+
}).passthrough();
|
|
326
|
+
var SquadSchema = z.object({
|
|
327
|
+
team: z.object({
|
|
328
|
+
name: z.string()
|
|
329
|
+
}).passthrough().optional(),
|
|
330
|
+
players: z.array(SquadPlayerItemSchema)
|
|
331
|
+
}).passthrough();
|
|
332
|
+
var SquadListSchema = z.object({
|
|
333
|
+
squad: SquadSchema
|
|
334
|
+
}).passthrough();
|
|
335
|
+
var WinLossRecordSchema = z.object({
|
|
336
|
+
wins: z.number(),
|
|
337
|
+
losses: z.number(),
|
|
338
|
+
draws: z.number(),
|
|
339
|
+
played: z.number().optional()
|
|
340
|
+
}).passthrough();
|
|
341
|
+
var LadderEntryRawSchema = z.object({
|
|
342
|
+
position: z.number(),
|
|
343
|
+
team: z.object({
|
|
344
|
+
name: z.string(),
|
|
345
|
+
id: z.number().optional(),
|
|
346
|
+
abbreviation: z.string().optional()
|
|
347
|
+
}).passthrough(),
|
|
348
|
+
played: z.number().optional(),
|
|
349
|
+
pointsFor: z.number().optional(),
|
|
350
|
+
pointsAgainst: z.number().optional(),
|
|
351
|
+
thisSeasonRecord: z.object({
|
|
352
|
+
aggregatePoints: z.number().optional(),
|
|
353
|
+
percentage: z.number().optional(),
|
|
354
|
+
winLossRecord: WinLossRecordSchema.optional()
|
|
355
|
+
}).passthrough().optional(),
|
|
356
|
+
form: z.string().optional()
|
|
357
|
+
}).passthrough();
|
|
358
|
+
var LadderResponseSchema = z.object({
|
|
359
|
+
ladders: z.array(
|
|
360
|
+
z.object({
|
|
361
|
+
entries: z.array(LadderEntryRawSchema)
|
|
362
|
+
}).passthrough()
|
|
363
|
+
),
|
|
364
|
+
round: z.object({
|
|
365
|
+
roundNumber: z.number(),
|
|
366
|
+
name: z.string().optional()
|
|
367
|
+
}).passthrough().optional()
|
|
368
|
+
}).passthrough();
|
|
369
|
+
|
|
370
|
+
// src/sources/afl-api.ts
|
|
371
|
+
var TOKEN_URL = "https://api.afl.com.au/cfs/afl/WMCTok";
|
|
372
|
+
var API_BASE = "https://aflapi.afl.com.au/afl/v2";
|
|
373
|
+
var CFS_BASE = "https://api.afl.com.au/cfs/afl";
|
|
374
|
+
var AflApiClient = class {
|
|
375
|
+
fetchFn;
|
|
376
|
+
tokenUrl;
|
|
377
|
+
cachedToken = null;
|
|
378
|
+
constructor(options) {
|
|
379
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
380
|
+
this.tokenUrl = options?.tokenUrl ?? TOKEN_URL;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Authenticate with the WMCTok token endpoint and cache the token.
|
|
384
|
+
*
|
|
385
|
+
* @returns The access token on success, or an error Result.
|
|
386
|
+
*/
|
|
387
|
+
async authenticate() {
|
|
388
|
+
try {
|
|
389
|
+
const response = await this.fetchFn(this.tokenUrl, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: { "Content-Length": "0" }
|
|
392
|
+
});
|
|
393
|
+
if (!response.ok) {
|
|
394
|
+
return err(new AflApiError(`Token request failed: ${response.status}`, response.status));
|
|
395
|
+
}
|
|
396
|
+
const json = await response.json();
|
|
397
|
+
const parsed = AflApiTokenSchema.safeParse(json);
|
|
398
|
+
if (!parsed.success) {
|
|
399
|
+
return err(new AflApiError("Invalid token response format"));
|
|
400
|
+
}
|
|
401
|
+
const ttlMs = 30 * 60 * 1e3;
|
|
402
|
+
this.cachedToken = {
|
|
403
|
+
accessToken: parsed.data.token,
|
|
404
|
+
expiresAt: Date.now() + ttlMs
|
|
405
|
+
};
|
|
406
|
+
return ok(parsed.data.token);
|
|
407
|
+
} catch (cause) {
|
|
408
|
+
return err(
|
|
409
|
+
new AflApiError(
|
|
410
|
+
`Token request failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
411
|
+
)
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Whether the cached token is still valid (not expired).
|
|
417
|
+
*/
|
|
418
|
+
get isAuthenticated() {
|
|
419
|
+
return this.cachedToken !== null && Date.now() < this.cachedToken.expiresAt;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Perform an authenticated fetch, automatically adding the bearer token.
|
|
423
|
+
* Retries once on 401 by re-authenticating.
|
|
424
|
+
*
|
|
425
|
+
* @param url - The URL to fetch.
|
|
426
|
+
* @param init - Additional fetch options.
|
|
427
|
+
* @returns The Response on success, or an error Result.
|
|
428
|
+
*/
|
|
429
|
+
async authedFetch(url, init) {
|
|
430
|
+
if (!this.isAuthenticated) {
|
|
431
|
+
const authResult = await this.authenticate();
|
|
432
|
+
if (!authResult.success) {
|
|
433
|
+
return authResult;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const doFetch = async () => {
|
|
437
|
+
const token = this.cachedToken;
|
|
438
|
+
if (!token) {
|
|
439
|
+
throw new AflApiError("No cached token available");
|
|
440
|
+
}
|
|
441
|
+
const headers = new Headers(init?.headers);
|
|
442
|
+
headers.set("x-media-mis-token", token.accessToken);
|
|
443
|
+
return this.fetchFn(url, { ...init, headers });
|
|
444
|
+
};
|
|
445
|
+
try {
|
|
446
|
+
let response = await doFetch();
|
|
447
|
+
if (response.status === 401) {
|
|
448
|
+
const authResult = await this.authenticate();
|
|
449
|
+
if (!authResult.success) {
|
|
450
|
+
return authResult;
|
|
451
|
+
}
|
|
452
|
+
response = await doFetch();
|
|
453
|
+
}
|
|
454
|
+
if (!response.ok) {
|
|
455
|
+
return err(
|
|
456
|
+
new AflApiError(
|
|
457
|
+
`Request failed: ${response.status} ${response.statusText}`,
|
|
458
|
+
response.status
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return ok(response);
|
|
463
|
+
} catch (cause) {
|
|
464
|
+
return err(
|
|
465
|
+
new AflApiError(
|
|
466
|
+
`Request failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
467
|
+
)
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Fetch JSON from a URL, validate with a Zod schema, and return a typed Result.
|
|
473
|
+
*
|
|
474
|
+
* @param url - The URL to fetch.
|
|
475
|
+
* @param schema - Zod schema to validate the response against.
|
|
476
|
+
* @returns Validated data on success, or an error Result.
|
|
477
|
+
*/
|
|
478
|
+
async fetchJson(url, schema) {
|
|
479
|
+
const isPublic = url.startsWith(API_BASE);
|
|
480
|
+
let response;
|
|
481
|
+
if (isPublic) {
|
|
482
|
+
try {
|
|
483
|
+
response = await this.fetchFn(url);
|
|
484
|
+
if (!response.ok) {
|
|
485
|
+
return err(
|
|
486
|
+
new AflApiError(
|
|
487
|
+
`Request failed: ${response.status} ${response.statusText}`,
|
|
488
|
+
response.status
|
|
489
|
+
)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
} catch (cause) {
|
|
493
|
+
return err(
|
|
494
|
+
new AflApiError(
|
|
495
|
+
`Request failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
496
|
+
)
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
const fetchResult = await this.authedFetch(url);
|
|
501
|
+
if (!fetchResult.success) {
|
|
502
|
+
return fetchResult;
|
|
503
|
+
}
|
|
504
|
+
response = fetchResult.data;
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
const json = await response.json();
|
|
508
|
+
const parsed = schema.safeParse(json);
|
|
509
|
+
if (!parsed.success) {
|
|
510
|
+
return err(
|
|
511
|
+
new ValidationError("Response validation failed", [
|
|
512
|
+
{ path: url, message: String(parsed.error) }
|
|
513
|
+
])
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return ok(parsed.data);
|
|
517
|
+
} catch (cause) {
|
|
518
|
+
return err(
|
|
519
|
+
new AflApiError(
|
|
520
|
+
`JSON parse failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Resolve a competition code (e.g. "AFLM") to its API competition ID.
|
|
527
|
+
*
|
|
528
|
+
* @param code - The competition code to resolve.
|
|
529
|
+
* @returns The competition ID string on success.
|
|
530
|
+
*/
|
|
531
|
+
async resolveCompetitionId(code) {
|
|
532
|
+
const result = await this.fetchJson(
|
|
533
|
+
`${API_BASE}/competitions?pageSize=50`,
|
|
534
|
+
CompetitionListSchema
|
|
535
|
+
);
|
|
536
|
+
if (!result.success) {
|
|
537
|
+
return result;
|
|
538
|
+
}
|
|
539
|
+
const apiCode = code === "AFLM" ? "AFL" : code;
|
|
540
|
+
const competition = result.data.competitions.find((c) => c.code === apiCode);
|
|
541
|
+
if (!competition) {
|
|
542
|
+
return err(new AflApiError(`Competition not found for code: ${code}`));
|
|
543
|
+
}
|
|
544
|
+
return ok(competition.id);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Resolve a season (compseason) ID from a competition ID and year.
|
|
548
|
+
*
|
|
549
|
+
* @param competitionId - The competition ID (from {@link resolveCompetitionId}).
|
|
550
|
+
* @param year - The season year (e.g. 2024).
|
|
551
|
+
* @returns The compseason ID string on success.
|
|
552
|
+
*/
|
|
553
|
+
async resolveSeasonId(competitionId, year) {
|
|
554
|
+
const result = await this.fetchJson(
|
|
555
|
+
`${API_BASE}/competitions/${competitionId}/compseasons?pageSize=100`,
|
|
556
|
+
CompseasonListSchema
|
|
557
|
+
);
|
|
558
|
+
if (!result.success) {
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
const yearStr = String(year);
|
|
562
|
+
const season = result.data.compSeasons.find((cs) => cs.name.includes(yearStr));
|
|
563
|
+
if (!season) {
|
|
564
|
+
return err(new AflApiError(`Season not found for year: ${year}`));
|
|
565
|
+
}
|
|
566
|
+
return ok(season.id);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Resolve a season ID from a competition code and year in one step.
|
|
570
|
+
*
|
|
571
|
+
* @param code - The competition code (e.g. "AFLM").
|
|
572
|
+
* @param year - The season year (e.g. 2025).
|
|
573
|
+
* @returns The compseason ID on success.
|
|
574
|
+
*/
|
|
575
|
+
async resolveCompSeason(code, year) {
|
|
576
|
+
const compResult = await this.resolveCompetitionId(code);
|
|
577
|
+
if (!compResult.success) return compResult;
|
|
578
|
+
return this.resolveSeasonId(compResult.data, year);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Fetch all rounds for a season with their metadata.
|
|
582
|
+
*
|
|
583
|
+
* @param seasonId - The compseason ID (from {@link resolveSeasonId}).
|
|
584
|
+
* @returns Array of round objects on success.
|
|
585
|
+
*/
|
|
586
|
+
async resolveRounds(seasonId) {
|
|
587
|
+
const result = await this.fetchJson(
|
|
588
|
+
`${API_BASE}/compseasons/${seasonId}/rounds?pageSize=50`,
|
|
589
|
+
RoundListSchema
|
|
590
|
+
);
|
|
591
|
+
if (!result.success) {
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
return ok(result.data.rounds);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Fetch match items for a round using the /cfs/ endpoint.
|
|
598
|
+
*
|
|
599
|
+
* @param roundProviderId - The round provider ID (e.g. "CD_R202501401").
|
|
600
|
+
* @returns Array of match items on success.
|
|
601
|
+
*/
|
|
602
|
+
async fetchRoundMatchItems(roundProviderId) {
|
|
603
|
+
const result = await this.fetchJson(
|
|
604
|
+
`${CFS_BASE}/matchItems/round/${roundProviderId}`,
|
|
605
|
+
MatchItemListSchema
|
|
606
|
+
);
|
|
607
|
+
if (!result.success) {
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
return ok(result.data.items);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Fetch match items for a round by resolving the round provider ID from season and round number.
|
|
614
|
+
*
|
|
615
|
+
* @param seasonId - The compseason ID.
|
|
616
|
+
* @param roundNumber - The round number.
|
|
617
|
+
* @returns Array of match items on success.
|
|
618
|
+
*/
|
|
619
|
+
async fetchRoundMatchItemsByNumber(seasonId, roundNumber) {
|
|
620
|
+
const roundsResult = await this.resolveRounds(seasonId);
|
|
621
|
+
if (!roundsResult.success) {
|
|
622
|
+
return roundsResult;
|
|
623
|
+
}
|
|
624
|
+
const round = roundsResult.data.find((r) => r.roundNumber === roundNumber);
|
|
625
|
+
if (!round?.providerId) {
|
|
626
|
+
return err(new AflApiError(`Round not found or missing providerId: round ${roundNumber}`));
|
|
627
|
+
}
|
|
628
|
+
return this.fetchRoundMatchItems(round.providerId);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Fetch match items for all completed rounds in a season.
|
|
632
|
+
*
|
|
633
|
+
* @param seasonId - The compseason ID.
|
|
634
|
+
* @returns Aggregated array of match items from all completed rounds.
|
|
635
|
+
*/
|
|
636
|
+
async fetchSeasonMatchItems(seasonId) {
|
|
637
|
+
const roundsResult = await this.resolveRounds(seasonId);
|
|
638
|
+
if (!roundsResult.success) {
|
|
639
|
+
return roundsResult;
|
|
640
|
+
}
|
|
641
|
+
const providerIds = roundsResult.data.flatMap((r) => r.providerId ? [r.providerId] : []);
|
|
642
|
+
const results = await Promise.all(providerIds.map((id) => this.fetchRoundMatchItems(id)));
|
|
643
|
+
const allItems = [];
|
|
644
|
+
for (const result of results) {
|
|
645
|
+
if (!result.success) {
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
const concluded = result.data.filter(
|
|
649
|
+
(item) => item.match.status === "CONCLUDED" || item.match.status === "COMPLETE"
|
|
650
|
+
);
|
|
651
|
+
allItems.push(...concluded);
|
|
652
|
+
}
|
|
653
|
+
return ok(allItems);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Fetch per-player statistics for a match.
|
|
657
|
+
*
|
|
658
|
+
* @param matchProviderId - The match provider ID (e.g. "CD_M20250140101").
|
|
659
|
+
* @returns Player stats list with home and away arrays.
|
|
660
|
+
*/
|
|
661
|
+
async fetchPlayerStats(matchProviderId) {
|
|
662
|
+
return this.fetchJson(
|
|
663
|
+
`${CFS_BASE}/playerStats/match/${matchProviderId}`,
|
|
664
|
+
PlayerStatsListSchema
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Fetch match roster (lineup) for a match.
|
|
669
|
+
*
|
|
670
|
+
* @param matchProviderId - The match provider ID (e.g. "CD_M20250140101").
|
|
671
|
+
* @returns Match roster with team players.
|
|
672
|
+
*/
|
|
673
|
+
async fetchMatchRoster(matchProviderId) {
|
|
674
|
+
return this.fetchJson(`${CFS_BASE}/matchRoster/full/${matchProviderId}`, MatchRosterSchema);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Fetch team list, optionally filtered by team type.
|
|
678
|
+
*
|
|
679
|
+
* @param teamType - Optional filter (e.g. "MEN", "WOMEN").
|
|
680
|
+
* @returns Array of team items.
|
|
681
|
+
*/
|
|
682
|
+
async fetchTeams(teamType) {
|
|
683
|
+
const result = await this.fetchJson(`${API_BASE}/teams?pageSize=100`, TeamListSchema);
|
|
684
|
+
if (!result.success) {
|
|
685
|
+
return result;
|
|
686
|
+
}
|
|
687
|
+
if (teamType) {
|
|
688
|
+
return ok(result.data.teams.filter((t) => t.teamType === teamType));
|
|
689
|
+
}
|
|
690
|
+
return ok(result.data.teams);
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Fetch squad (roster) for a team in a specific season.
|
|
694
|
+
*
|
|
695
|
+
* @param teamId - The numeric team ID.
|
|
696
|
+
* @param compSeasonId - The compseason ID.
|
|
697
|
+
* @returns Squad list response.
|
|
698
|
+
*/
|
|
699
|
+
async fetchSquad(teamId, compSeasonId) {
|
|
700
|
+
return this.fetchJson(
|
|
701
|
+
`${API_BASE}/squads?teamId=${teamId}&compSeasonId=${compSeasonId}`,
|
|
702
|
+
SquadListSchema
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Fetch ladder standings for a season (optionally for a specific round).
|
|
707
|
+
*
|
|
708
|
+
* @param seasonId - The compseason ID.
|
|
709
|
+
* @param roundId - Optional round ID (numeric `id`, not `providerId`).
|
|
710
|
+
* @returns Ladder response with entries.
|
|
711
|
+
*/
|
|
712
|
+
async fetchLadder(seasonId, roundId) {
|
|
713
|
+
let url = `${API_BASE}/compseasons/${seasonId}/ladders`;
|
|
714
|
+
if (roundId != null) {
|
|
715
|
+
url += `?roundId=${roundId}`;
|
|
716
|
+
}
|
|
717
|
+
return this.fetchJson(url, LadderResponseSchema);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// src/transforms/match-results.ts
|
|
722
|
+
var FINALS_PATTERN = /final|elimination|qualifying|preliminary|semi|grand/i;
|
|
723
|
+
function inferRoundType(roundName) {
|
|
724
|
+
return FINALS_PATTERN.test(roundName) ? "Finals" : "HomeAndAway";
|
|
725
|
+
}
|
|
726
|
+
function toMatchStatus(raw) {
|
|
727
|
+
switch (raw) {
|
|
728
|
+
case "CONCLUDED":
|
|
729
|
+
case "COMPLETE":
|
|
730
|
+
return "Complete";
|
|
731
|
+
case "LIVE":
|
|
732
|
+
case "IN_PROGRESS":
|
|
733
|
+
return "Live";
|
|
734
|
+
case "UPCOMING":
|
|
735
|
+
case "SCHEDULED":
|
|
736
|
+
return "Upcoming";
|
|
737
|
+
case "POSTPONED":
|
|
738
|
+
return "Postponed";
|
|
739
|
+
case "CANCELLED":
|
|
740
|
+
return "Cancelled";
|
|
741
|
+
default:
|
|
742
|
+
return "Complete";
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function toQuarterScore(period) {
|
|
746
|
+
return {
|
|
747
|
+
goals: period.score.goals,
|
|
748
|
+
behinds: period.score.behinds,
|
|
749
|
+
points: period.score.totalScore
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function findPeriod(periods, quarter) {
|
|
753
|
+
if (!periods) return null;
|
|
754
|
+
const period = periods.find((p) => p.periodNumber === quarter);
|
|
755
|
+
return period ? toQuarterScore(period) : null;
|
|
756
|
+
}
|
|
757
|
+
function transformMatchItems(items, season, competition, source = "afl-api") {
|
|
758
|
+
return items.map((item) => {
|
|
759
|
+
const homeScore = item.score?.homeTeamScore;
|
|
760
|
+
const awayScore = item.score?.awayTeamScore;
|
|
761
|
+
const homePoints = homeScore?.matchScore.totalScore ?? 0;
|
|
762
|
+
const awayPoints = awayScore?.matchScore.totalScore ?? 0;
|
|
763
|
+
return {
|
|
764
|
+
matchId: item.match.matchId,
|
|
765
|
+
season,
|
|
766
|
+
roundNumber: item.round?.roundNumber ?? 0,
|
|
767
|
+
roundType: inferRoundType(item.round?.name ?? ""),
|
|
768
|
+
date: new Date(item.match.utcStartTime),
|
|
769
|
+
venue: item.venue?.name ?? "",
|
|
770
|
+
homeTeam: normaliseTeamName(item.match.homeTeam.name),
|
|
771
|
+
awayTeam: normaliseTeamName(item.match.awayTeam.name),
|
|
772
|
+
homeGoals: homeScore?.matchScore.goals ?? 0,
|
|
773
|
+
homeBehinds: homeScore?.matchScore.behinds ?? 0,
|
|
774
|
+
homePoints,
|
|
775
|
+
awayGoals: awayScore?.matchScore.goals ?? 0,
|
|
776
|
+
awayBehinds: awayScore?.matchScore.behinds ?? 0,
|
|
777
|
+
awayPoints,
|
|
778
|
+
margin: homePoints - awayPoints,
|
|
779
|
+
q1Home: findPeriod(homeScore?.periodScore, 1),
|
|
780
|
+
q2Home: findPeriod(homeScore?.periodScore, 2),
|
|
781
|
+
q3Home: findPeriod(homeScore?.periodScore, 3),
|
|
782
|
+
q4Home: findPeriod(homeScore?.periodScore, 4),
|
|
783
|
+
q1Away: findPeriod(awayScore?.periodScore, 1),
|
|
784
|
+
q2Away: findPeriod(awayScore?.periodScore, 2),
|
|
785
|
+
q3Away: findPeriod(awayScore?.periodScore, 3),
|
|
786
|
+
q4Away: findPeriod(awayScore?.periodScore, 4),
|
|
787
|
+
status: toMatchStatus(item.match.status),
|
|
788
|
+
attendance: null,
|
|
789
|
+
venueState: item.venue?.state ?? null,
|
|
790
|
+
venueTimezone: item.venue?.timeZone ?? null,
|
|
791
|
+
homeRushedBehinds: homeScore?.rushedBehinds ?? null,
|
|
792
|
+
awayRushedBehinds: awayScore?.rushedBehinds ?? null,
|
|
793
|
+
homeMinutesInFront: homeScore?.minutesInFront ?? null,
|
|
794
|
+
awayMinutesInFront: awayScore?.minutesInFront ?? null,
|
|
795
|
+
source,
|
|
796
|
+
competition
|
|
797
|
+
};
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/api/fixture.ts
|
|
802
|
+
function toFixture(item, season, fallbackRoundNumber, competition) {
|
|
803
|
+
return {
|
|
804
|
+
matchId: item.match.matchId,
|
|
805
|
+
season,
|
|
806
|
+
roundNumber: item.round?.roundNumber ?? fallbackRoundNumber,
|
|
807
|
+
roundType: inferRoundType(item.round?.name ?? ""),
|
|
808
|
+
date: new Date(item.match.utcStartTime),
|
|
809
|
+
venue: item.venue?.name ?? "",
|
|
810
|
+
homeTeam: normaliseTeamName(item.match.homeTeam.name),
|
|
811
|
+
awayTeam: normaliseTeamName(item.match.awayTeam.name),
|
|
812
|
+
status: toMatchStatus(item.match.status),
|
|
813
|
+
competition
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
async function fetchFixture(query) {
|
|
817
|
+
const competition = query.competition ?? "AFLM";
|
|
818
|
+
if (query.source !== "afl-api") {
|
|
819
|
+
return err(
|
|
820
|
+
new UnsupportedSourceError(
|
|
821
|
+
"Fixture data is only available from the AFL API source.",
|
|
822
|
+
query.source
|
|
823
|
+
)
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
const client = new AflApiClient();
|
|
827
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
828
|
+
if (!seasonResult.success) return seasonResult;
|
|
829
|
+
if (query.round != null) {
|
|
830
|
+
const itemsResult = await client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round);
|
|
831
|
+
if (!itemsResult.success) return itemsResult;
|
|
832
|
+
return ok(itemsResult.data.map((item) => toFixture(item, query.season, 0, competition)));
|
|
833
|
+
}
|
|
834
|
+
const roundsResult = await client.resolveRounds(seasonResult.data);
|
|
835
|
+
if (!roundsResult.success) return roundsResult;
|
|
836
|
+
const roundProviderIds = roundsResult.data.flatMap(
|
|
837
|
+
(r) => r.providerId ? [{ providerId: r.providerId, roundNumber: r.roundNumber }] : []
|
|
838
|
+
);
|
|
839
|
+
const roundResults = await Promise.all(
|
|
840
|
+
roundProviderIds.map((r) => client.fetchRoundMatchItems(r.providerId))
|
|
841
|
+
);
|
|
842
|
+
const fixtures = [];
|
|
843
|
+
for (let i = 0; i < roundResults.length; i++) {
|
|
844
|
+
const result = roundResults[i];
|
|
845
|
+
if (!result?.success) continue;
|
|
846
|
+
const roundNumber = roundProviderIds[i]?.roundNumber ?? 0;
|
|
847
|
+
for (const item of result.data) {
|
|
848
|
+
fixtures.push(toFixture(item, query.season, roundNumber, competition));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return ok(fixtures);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/transforms/ladder.ts
|
|
855
|
+
function transformLadderEntries(entries) {
|
|
856
|
+
return entries.map((entry) => {
|
|
857
|
+
const record = entry.thisSeasonRecord;
|
|
858
|
+
const wl = record?.winLossRecord;
|
|
859
|
+
return {
|
|
860
|
+
position: entry.position,
|
|
861
|
+
team: normaliseTeamName(entry.team.name),
|
|
862
|
+
played: entry.played ?? wl?.played ?? 0,
|
|
863
|
+
wins: wl?.wins ?? 0,
|
|
864
|
+
losses: wl?.losses ?? 0,
|
|
865
|
+
draws: wl?.draws ?? 0,
|
|
866
|
+
pointsFor: entry.pointsFor ?? 0,
|
|
867
|
+
pointsAgainst: entry.pointsAgainst ?? 0,
|
|
868
|
+
percentage: record?.percentage ?? 0,
|
|
869
|
+
premiershipsPoints: record?.aggregatePoints ?? 0,
|
|
870
|
+
form: entry.form ?? null
|
|
871
|
+
};
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/api/ladder.ts
|
|
876
|
+
async function fetchLadder(query) {
|
|
877
|
+
const competition = query.competition ?? "AFLM";
|
|
878
|
+
if (query.source !== "afl-api") {
|
|
879
|
+
return err(
|
|
880
|
+
new UnsupportedSourceError(
|
|
881
|
+
"Ladder data is only available from the AFL API source.",
|
|
882
|
+
query.source
|
|
883
|
+
)
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
const client = new AflApiClient();
|
|
887
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
888
|
+
if (!seasonResult.success) return seasonResult;
|
|
889
|
+
let roundId;
|
|
890
|
+
if (query.round != null) {
|
|
891
|
+
const roundsResult = await client.resolveRounds(seasonResult.data);
|
|
892
|
+
if (!roundsResult.success) return roundsResult;
|
|
893
|
+
const round = roundsResult.data.find((r) => r.roundNumber === query.round);
|
|
894
|
+
if (round) {
|
|
895
|
+
roundId = round.id;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const ladderResult = await client.fetchLadder(seasonResult.data, roundId);
|
|
899
|
+
if (!ladderResult.success) return ladderResult;
|
|
900
|
+
const firstLadder = ladderResult.data.ladders[0];
|
|
901
|
+
const entries = firstLadder ? transformLadderEntries(firstLadder.entries) : [];
|
|
902
|
+
return ok({
|
|
903
|
+
season: query.season,
|
|
904
|
+
roundNumber: ladderResult.data.round?.roundNumber ?? null,
|
|
905
|
+
entries,
|
|
906
|
+
competition
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/transforms/lineup.ts
|
|
911
|
+
var EMERGENCY_POSITIONS = /* @__PURE__ */ new Set(["EMG", "EMERG"]);
|
|
912
|
+
var SUBSTITUTE_POSITIONS = /* @__PURE__ */ new Set(["SUB", "INT"]);
|
|
913
|
+
function transformMatchRoster(roster, season, roundNumber, competition) {
|
|
914
|
+
const homeTeamId = roster.match.homeTeamId;
|
|
915
|
+
const awayTeamId = roster.match.awayTeamId;
|
|
916
|
+
const homeTeamPlayers = roster.teamPlayers.find((tp) => tp.teamId === homeTeamId);
|
|
917
|
+
const awayTeamPlayers = roster.teamPlayers.find((tp) => tp.teamId === awayTeamId);
|
|
918
|
+
const mapPlayers = (players) => players.map((p) => {
|
|
919
|
+
const inner = p.player.player;
|
|
920
|
+
const position = p.player.position ?? null;
|
|
921
|
+
return {
|
|
922
|
+
playerId: inner.playerId,
|
|
923
|
+
givenName: inner.playerName.givenName,
|
|
924
|
+
surname: inner.playerName.surname,
|
|
925
|
+
displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
|
|
926
|
+
jumperNumber: p.jumperNumber ?? null,
|
|
927
|
+
position,
|
|
928
|
+
isEmergency: position !== null && EMERGENCY_POSITIONS.has(position),
|
|
929
|
+
isSubstitute: position !== null && SUBSTITUTE_POSITIONS.has(position)
|
|
930
|
+
};
|
|
931
|
+
});
|
|
932
|
+
return {
|
|
933
|
+
matchId: roster.match.matchId,
|
|
934
|
+
season,
|
|
935
|
+
roundNumber,
|
|
936
|
+
homeTeam: normaliseTeamName(roster.match.homeTeam.name),
|
|
937
|
+
awayTeam: normaliseTeamName(roster.match.awayTeam.name),
|
|
938
|
+
homePlayers: homeTeamPlayers ? mapPlayers(homeTeamPlayers.players) : [],
|
|
939
|
+
awayPlayers: awayTeamPlayers ? mapPlayers(awayTeamPlayers.players) : [],
|
|
940
|
+
competition
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/api/lineup.ts
|
|
945
|
+
async function fetchLineup(query) {
|
|
946
|
+
const competition = query.competition ?? "AFLM";
|
|
947
|
+
if (query.source !== "afl-api") {
|
|
948
|
+
return err(
|
|
949
|
+
new UnsupportedSourceError(
|
|
950
|
+
"Lineup data is only available from the AFL API source.",
|
|
951
|
+
query.source
|
|
952
|
+
)
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
const client = new AflApiClient();
|
|
956
|
+
if (query.matchId) {
|
|
957
|
+
const rosterResult = await client.fetchMatchRoster(query.matchId);
|
|
958
|
+
if (!rosterResult.success) return rosterResult;
|
|
959
|
+
return ok([transformMatchRoster(rosterResult.data, query.season, query.round, competition)]);
|
|
960
|
+
}
|
|
961
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
962
|
+
if (!seasonResult.success) return seasonResult;
|
|
963
|
+
const matchItems = await client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round);
|
|
964
|
+
if (!matchItems.success) return matchItems;
|
|
965
|
+
if (matchItems.data.length === 0) {
|
|
966
|
+
return err(new AflApiError(`No matches found for round ${query.round}`));
|
|
967
|
+
}
|
|
968
|
+
const rosterResults = await Promise.all(
|
|
969
|
+
matchItems.data.map((item) => client.fetchMatchRoster(item.match.matchId))
|
|
970
|
+
);
|
|
971
|
+
const lineups = [];
|
|
972
|
+
for (const rosterResult of rosterResults) {
|
|
973
|
+
if (!rosterResult.success) return rosterResult;
|
|
974
|
+
lineups.push(transformMatchRoster(rosterResult.data, query.season, query.round, competition));
|
|
975
|
+
}
|
|
976
|
+
return ok(lineups);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/sources/afl-tables.ts
|
|
980
|
+
import * as cheerio from "cheerio";
|
|
981
|
+
|
|
982
|
+
// src/lib/date-utils.ts
|
|
983
|
+
function parseAflApiDate(iso) {
|
|
984
|
+
const date = new Date(iso);
|
|
985
|
+
if (Number.isNaN(date.getTime())) {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
return date;
|
|
989
|
+
}
|
|
990
|
+
function parseFootyWireDate(dateStr) {
|
|
991
|
+
const trimmed = dateStr.trim();
|
|
992
|
+
if (trimmed === "") {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
|
|
996
|
+
const normalised = withoutDow.replace(/-/g, " ");
|
|
997
|
+
const match = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
|
|
998
|
+
if (!match) {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
const [, dayStr, monthStr, yearStr] = match;
|
|
1002
|
+
if (!dayStr || !monthStr || !yearStr) {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
|
|
1006
|
+
if (monthIndex === void 0) {
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
const year = Number.parseInt(yearStr, 10);
|
|
1010
|
+
const day = Number.parseInt(dayStr, 10);
|
|
1011
|
+
const date = new Date(Date.UTC(year, monthIndex, day));
|
|
1012
|
+
if (Number.isNaN(date.getTime())) {
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
return date;
|
|
1019
|
+
}
|
|
1020
|
+
function parseAflTablesDate(dateStr) {
|
|
1021
|
+
const trimmed = dateStr.trim();
|
|
1022
|
+
if (trimmed === "") {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
|
|
1026
|
+
const normalised = withoutDow.replace(/[-/]/g, " ");
|
|
1027
|
+
const dmy = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
|
|
1028
|
+
if (dmy) {
|
|
1029
|
+
const [, dayStr, monthStr, yearStr] = dmy;
|
|
1030
|
+
if (dayStr && monthStr && yearStr) {
|
|
1031
|
+
return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const mdy = /^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})$/.exec(normalised);
|
|
1035
|
+
if (mdy) {
|
|
1036
|
+
const [, monthStr, dayStr, yearStr] = mdy;
|
|
1037
|
+
if (dayStr && monthStr && yearStr) {
|
|
1038
|
+
return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
function toAestString(date) {
|
|
1044
|
+
const formatter = new Intl.DateTimeFormat("en-AU", {
|
|
1045
|
+
timeZone: "Australia/Melbourne",
|
|
1046
|
+
weekday: "short",
|
|
1047
|
+
day: "numeric",
|
|
1048
|
+
month: "short",
|
|
1049
|
+
year: "numeric",
|
|
1050
|
+
hour: "numeric",
|
|
1051
|
+
minute: "2-digit",
|
|
1052
|
+
hour12: true,
|
|
1053
|
+
timeZoneName: "short"
|
|
1054
|
+
});
|
|
1055
|
+
return formatter.format(date);
|
|
1056
|
+
}
|
|
1057
|
+
var MONTH_ABBREV_TO_INDEX = /* @__PURE__ */ new Map([
|
|
1058
|
+
["jan", 0],
|
|
1059
|
+
["feb", 1],
|
|
1060
|
+
["mar", 2],
|
|
1061
|
+
["apr", 3],
|
|
1062
|
+
["may", 4],
|
|
1063
|
+
["jun", 5],
|
|
1064
|
+
["jul", 6],
|
|
1065
|
+
["aug", 7],
|
|
1066
|
+
["sep", 8],
|
|
1067
|
+
["oct", 9],
|
|
1068
|
+
["nov", 10],
|
|
1069
|
+
["dec", 11],
|
|
1070
|
+
["january", 0],
|
|
1071
|
+
["february", 1],
|
|
1072
|
+
["march", 2],
|
|
1073
|
+
["april", 3],
|
|
1074
|
+
["june", 5],
|
|
1075
|
+
["july", 6],
|
|
1076
|
+
["august", 7],
|
|
1077
|
+
["september", 8],
|
|
1078
|
+
["october", 9],
|
|
1079
|
+
["november", 10],
|
|
1080
|
+
["december", 11]
|
|
1081
|
+
]);
|
|
1082
|
+
function buildUtcDate(year, monthStr, day) {
|
|
1083
|
+
const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
|
|
1084
|
+
if (monthIndex === void 0) {
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1087
|
+
const date = new Date(Date.UTC(year, monthIndex, day));
|
|
1088
|
+
if (Number.isNaN(date.getTime())) {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
return date;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/sources/afl-tables.ts
|
|
1098
|
+
var AFL_TABLES_BASE = "https://afltables.com/afl/seas";
|
|
1099
|
+
var AflTablesClient = class {
|
|
1100
|
+
fetchFn;
|
|
1101
|
+
constructor(options) {
|
|
1102
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Fetch season match results from AFL Tables.
|
|
1106
|
+
*
|
|
1107
|
+
* @param year - The season year (1897 to present).
|
|
1108
|
+
* @returns Array of match results.
|
|
1109
|
+
*/
|
|
1110
|
+
async fetchSeasonResults(year) {
|
|
1111
|
+
const url = `${AFL_TABLES_BASE}/${year}.html`;
|
|
1112
|
+
try {
|
|
1113
|
+
const response = await this.fetchFn(url, {
|
|
1114
|
+
headers: { "User-Agent": "Mozilla/5.0" }
|
|
1115
|
+
});
|
|
1116
|
+
if (!response.ok) {
|
|
1117
|
+
return err(
|
|
1118
|
+
new ScrapeError(`AFL Tables request failed: ${response.status} (${url})`, "afl-tables")
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
const html = await response.text();
|
|
1122
|
+
const results = parseSeasonPage(html, year);
|
|
1123
|
+
return ok(results);
|
|
1124
|
+
} catch (cause) {
|
|
1125
|
+
return err(
|
|
1126
|
+
new ScrapeError(
|
|
1127
|
+
`AFL Tables request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1128
|
+
"afl-tables"
|
|
1129
|
+
)
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
function parseSeasonPage(html, year) {
|
|
1135
|
+
const $ = cheerio.load(html);
|
|
1136
|
+
const results = [];
|
|
1137
|
+
let currentRound = 0;
|
|
1138
|
+
let currentRoundType = "HomeAndAway";
|
|
1139
|
+
let matchCounter = 0;
|
|
1140
|
+
$("table").each((_i, table) => {
|
|
1141
|
+
const $table = $(table);
|
|
1142
|
+
const text = $table.text().trim();
|
|
1143
|
+
const roundMatch = /^Round\s+(\d+)/i.exec(text);
|
|
1144
|
+
if (roundMatch?.[1] && !$table.attr("border")) {
|
|
1145
|
+
currentRound = Number.parseInt(roundMatch[1], 10);
|
|
1146
|
+
currentRoundType = inferRoundType(text);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (!$table.attr("border") && inferRoundType(text) === "Finals") {
|
|
1150
|
+
currentRoundType = "Finals";
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if ($table.attr("border") !== "1") return;
|
|
1154
|
+
const rows = $table.find("tr");
|
|
1155
|
+
if (rows.length !== 2) return;
|
|
1156
|
+
const homeRow = $(rows[0]);
|
|
1157
|
+
const awayRow = $(rows[1]);
|
|
1158
|
+
const homeCells = homeRow.find("td");
|
|
1159
|
+
const awayCells = awayRow.find("td");
|
|
1160
|
+
if (homeCells.length < 4 || awayCells.length < 3) return;
|
|
1161
|
+
const homeTeam = normaliseTeamName($(homeCells[0]).find("a").text().trim());
|
|
1162
|
+
const awayTeam = normaliseTeamName($(awayCells[0]).find("a").text().trim());
|
|
1163
|
+
if (!homeTeam || !awayTeam) return;
|
|
1164
|
+
const homeQuarters = parseQuarterScores($(homeCells[1]).text());
|
|
1165
|
+
const awayQuarters = parseQuarterScores($(awayCells[1]).text());
|
|
1166
|
+
const homePoints = Number.parseInt($(homeCells[2]).text().trim(), 10) || 0;
|
|
1167
|
+
const awayPoints = Number.parseInt($(awayCells[2]).text().trim(), 10) || 0;
|
|
1168
|
+
const infoText = $(homeCells[3]).text().trim();
|
|
1169
|
+
const date = parseDateFromInfo(infoText, year);
|
|
1170
|
+
const venue = parseVenueFromInfo($(homeCells[3]).html() ?? "");
|
|
1171
|
+
const attendance = parseAttendanceFromInfo(infoText);
|
|
1172
|
+
const homeFinal = homeQuarters[3];
|
|
1173
|
+
const awayFinal = awayQuarters[3];
|
|
1174
|
+
matchCounter++;
|
|
1175
|
+
results.push({
|
|
1176
|
+
matchId: `AT_${year}_${matchCounter}`,
|
|
1177
|
+
season: year,
|
|
1178
|
+
roundNumber: currentRound,
|
|
1179
|
+
roundType: currentRoundType,
|
|
1180
|
+
date,
|
|
1181
|
+
venue,
|
|
1182
|
+
homeTeam,
|
|
1183
|
+
awayTeam,
|
|
1184
|
+
homeGoals: homeFinal?.goals ?? 0,
|
|
1185
|
+
homeBehinds: homeFinal?.behinds ?? 0,
|
|
1186
|
+
homePoints,
|
|
1187
|
+
awayGoals: awayFinal?.goals ?? 0,
|
|
1188
|
+
awayBehinds: awayFinal?.behinds ?? 0,
|
|
1189
|
+
awayPoints,
|
|
1190
|
+
margin: homePoints - awayPoints,
|
|
1191
|
+
q1Home: homeQuarters[0] ?? null,
|
|
1192
|
+
q2Home: homeQuarters[1] ?? null,
|
|
1193
|
+
q3Home: homeQuarters[2] ?? null,
|
|
1194
|
+
q4Home: homeQuarters[3] ?? null,
|
|
1195
|
+
q1Away: awayQuarters[0] ?? null,
|
|
1196
|
+
q2Away: awayQuarters[1] ?? null,
|
|
1197
|
+
q3Away: awayQuarters[2] ?? null,
|
|
1198
|
+
q4Away: awayQuarters[3] ?? null,
|
|
1199
|
+
status: "Complete",
|
|
1200
|
+
attendance,
|
|
1201
|
+
venueState: null,
|
|
1202
|
+
venueTimezone: null,
|
|
1203
|
+
homeRushedBehinds: null,
|
|
1204
|
+
awayRushedBehinds: null,
|
|
1205
|
+
homeMinutesInFront: null,
|
|
1206
|
+
awayMinutesInFront: null,
|
|
1207
|
+
source: "afl-tables",
|
|
1208
|
+
competition: "AFLM"
|
|
1209
|
+
});
|
|
1210
|
+
});
|
|
1211
|
+
return results;
|
|
1212
|
+
}
|
|
1213
|
+
function parseQuarterScores(text) {
|
|
1214
|
+
const clean = text.replace(/\u00a0/g, " ").trim();
|
|
1215
|
+
const matches = [...clean.matchAll(/(\d+)\.(\d+)/g)];
|
|
1216
|
+
return matches.map((m) => {
|
|
1217
|
+
const goals = Number.parseInt(m[1] ?? "0", 10);
|
|
1218
|
+
const behinds = Number.parseInt(m[2] ?? "0", 10);
|
|
1219
|
+
return { goals, behinds, points: goals * 6 + behinds };
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
function parseDateFromInfo(text, year) {
|
|
1223
|
+
const dateMatch = /(\d{1,2}-[A-Z][a-z]{2}-\d{4})/.exec(text);
|
|
1224
|
+
if (dateMatch?.[1]) {
|
|
1225
|
+
return parseAflTablesDate(dateMatch[1]) ?? new Date(year, 0, 1);
|
|
1226
|
+
}
|
|
1227
|
+
return parseAflTablesDate(text) ?? new Date(year, 0, 1);
|
|
1228
|
+
}
|
|
1229
|
+
function parseVenueFromInfo(html) {
|
|
1230
|
+
const $ = cheerio.load(html);
|
|
1231
|
+
const venueLink = $("a[href*='venues']");
|
|
1232
|
+
if (venueLink.length > 0) {
|
|
1233
|
+
return venueLink.text().trim();
|
|
1234
|
+
}
|
|
1235
|
+
const venueMatch = /Venue:\s*(.+?)(?:<|$)/i.exec(html);
|
|
1236
|
+
return venueMatch?.[1]?.trim() ?? "";
|
|
1237
|
+
}
|
|
1238
|
+
function parseAttendanceFromInfo(text) {
|
|
1239
|
+
const match = /Att:\s*([\d,]+)/i.exec(text);
|
|
1240
|
+
if (!match?.[1]) return null;
|
|
1241
|
+
return Number.parseInt(match[1].replace(/,/g, ""), 10) || null;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/sources/footywire.ts
|
|
1245
|
+
import * as cheerio2 from "cheerio";
|
|
1246
|
+
var FOOTYWIRE_BASE = "https://www.footywire.com/afl/footy";
|
|
1247
|
+
var FootyWireClient = class {
|
|
1248
|
+
fetchFn;
|
|
1249
|
+
constructor(options) {
|
|
1250
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Fetch the HTML content of a FootyWire page.
|
|
1254
|
+
*/
|
|
1255
|
+
async fetchHtml(url) {
|
|
1256
|
+
try {
|
|
1257
|
+
const response = await this.fetchFn(url, {
|
|
1258
|
+
headers: {
|
|
1259
|
+
"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"
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
if (!response.ok) {
|
|
1263
|
+
return err(
|
|
1264
|
+
new ScrapeError(`FootyWire request failed: ${response.status} (${url})`, "footywire")
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
const html = await response.text();
|
|
1268
|
+
return ok(html);
|
|
1269
|
+
} catch (cause) {
|
|
1270
|
+
return err(
|
|
1271
|
+
new ScrapeError(
|
|
1272
|
+
`FootyWire request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1273
|
+
"footywire"
|
|
1274
|
+
)
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Fetch season match results from FootyWire.
|
|
1280
|
+
*
|
|
1281
|
+
* @param year - The season year.
|
|
1282
|
+
* @returns Array of match results.
|
|
1283
|
+
*/
|
|
1284
|
+
async fetchSeasonResults(year) {
|
|
1285
|
+
const url = `${FOOTYWIRE_BASE}/ft_match_list?year=${year}`;
|
|
1286
|
+
const htmlResult = await this.fetchHtml(url);
|
|
1287
|
+
if (!htmlResult.success) {
|
|
1288
|
+
return htmlResult;
|
|
1289
|
+
}
|
|
1290
|
+
try {
|
|
1291
|
+
const results = parseMatchList(htmlResult.data, year);
|
|
1292
|
+
return ok(results);
|
|
1293
|
+
} catch (cause) {
|
|
1294
|
+
return err(
|
|
1295
|
+
new ScrapeError(
|
|
1296
|
+
`Failed to parse match list: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1297
|
+
"footywire"
|
|
1298
|
+
)
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
function parseMatchList(html, year) {
|
|
1304
|
+
const $ = cheerio2.load(html);
|
|
1305
|
+
const results = [];
|
|
1306
|
+
let currentRound = 0;
|
|
1307
|
+
let currentRoundType = "HomeAndAway";
|
|
1308
|
+
$("tr").each((_i, row) => {
|
|
1309
|
+
const roundHeader = $(row).find("td[colspan='7']");
|
|
1310
|
+
if (roundHeader.length > 0) {
|
|
1311
|
+
const text = roundHeader.text().trim();
|
|
1312
|
+
currentRoundType = inferRoundType(text);
|
|
1313
|
+
const roundMatch = /Round\s+(\d+)/i.exec(text);
|
|
1314
|
+
if (roundMatch?.[1]) {
|
|
1315
|
+
currentRound = Number.parseInt(roundMatch[1], 10);
|
|
1316
|
+
}
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const cells = $(row).find("td.data");
|
|
1320
|
+
if (cells.length < 5) return;
|
|
1321
|
+
const dateText = $(cells[0]).text().trim();
|
|
1322
|
+
const teamsCell = $(cells[1]);
|
|
1323
|
+
const venue = $(cells[2]).text().trim();
|
|
1324
|
+
const attendance = $(cells[3]).text().trim();
|
|
1325
|
+
const scoreCell = $(cells[4]);
|
|
1326
|
+
if (venue === "BYE") return;
|
|
1327
|
+
const teamLinks = teamsCell.find("a");
|
|
1328
|
+
if (teamLinks.length < 2) return;
|
|
1329
|
+
const homeTeam = normaliseTeamName($(teamLinks[0]).text().trim());
|
|
1330
|
+
const awayTeam = normaliseTeamName($(teamLinks[1]).text().trim());
|
|
1331
|
+
const scoreText = scoreCell.text().trim();
|
|
1332
|
+
const scoreMatch = /(\d+)-(\d+)/.exec(scoreText);
|
|
1333
|
+
if (!scoreMatch) return;
|
|
1334
|
+
const homePoints = Number.parseInt(scoreMatch[1] ?? "0", 10);
|
|
1335
|
+
const awayPoints = Number.parseInt(scoreMatch[2] ?? "0", 10);
|
|
1336
|
+
const scoreLink = scoreCell.find("a").attr("href") ?? "";
|
|
1337
|
+
const midMatch = /mid=(\d+)/.exec(scoreLink);
|
|
1338
|
+
const matchId = midMatch?.[1] ? `FW_${midMatch[1]}` : `FW_${year}_R${currentRound}_${homeTeam}`;
|
|
1339
|
+
const date = parseFootyWireDate(dateText) ?? new Date(year, 0, 1);
|
|
1340
|
+
const homeGoals = Math.floor(homePoints / 6);
|
|
1341
|
+
const homeBehinds = homePoints - homeGoals * 6;
|
|
1342
|
+
const awayGoals = Math.floor(awayPoints / 6);
|
|
1343
|
+
const awayBehinds = awayPoints - awayGoals * 6;
|
|
1344
|
+
results.push({
|
|
1345
|
+
matchId,
|
|
1346
|
+
season: year,
|
|
1347
|
+
roundNumber: currentRound,
|
|
1348
|
+
roundType: currentRoundType,
|
|
1349
|
+
date,
|
|
1350
|
+
venue,
|
|
1351
|
+
homeTeam,
|
|
1352
|
+
awayTeam,
|
|
1353
|
+
homeGoals,
|
|
1354
|
+
homeBehinds,
|
|
1355
|
+
homePoints,
|
|
1356
|
+
awayGoals,
|
|
1357
|
+
awayBehinds,
|
|
1358
|
+
awayPoints,
|
|
1359
|
+
margin: homePoints - awayPoints,
|
|
1360
|
+
q1Home: null,
|
|
1361
|
+
q2Home: null,
|
|
1362
|
+
q3Home: null,
|
|
1363
|
+
q4Home: null,
|
|
1364
|
+
q1Away: null,
|
|
1365
|
+
q2Away: null,
|
|
1366
|
+
q3Away: null,
|
|
1367
|
+
q4Away: null,
|
|
1368
|
+
status: "Complete",
|
|
1369
|
+
attendance: attendance ? Number.parseInt(attendance, 10) || null : null,
|
|
1370
|
+
venueState: null,
|
|
1371
|
+
venueTimezone: null,
|
|
1372
|
+
homeRushedBehinds: null,
|
|
1373
|
+
awayRushedBehinds: null,
|
|
1374
|
+
homeMinutesInFront: null,
|
|
1375
|
+
awayMinutesInFront: null,
|
|
1376
|
+
source: "footywire",
|
|
1377
|
+
competition: "AFLM"
|
|
1378
|
+
});
|
|
1379
|
+
});
|
|
1380
|
+
return results;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// src/api/match-results.ts
|
|
1384
|
+
async function fetchMatchResults(query) {
|
|
1385
|
+
const competition = query.competition ?? "AFLM";
|
|
1386
|
+
switch (query.source) {
|
|
1387
|
+
case "afl-api": {
|
|
1388
|
+
const client = new AflApiClient();
|
|
1389
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
1390
|
+
if (!seasonResult.success) return seasonResult;
|
|
1391
|
+
if (query.round != null) {
|
|
1392
|
+
const itemsResult2 = await client.fetchRoundMatchItemsByNumber(
|
|
1393
|
+
seasonResult.data,
|
|
1394
|
+
query.round
|
|
1395
|
+
);
|
|
1396
|
+
if (!itemsResult2.success) return itemsResult2;
|
|
1397
|
+
return ok(transformMatchItems(itemsResult2.data, query.season, competition));
|
|
1398
|
+
}
|
|
1399
|
+
const itemsResult = await client.fetchSeasonMatchItems(seasonResult.data);
|
|
1400
|
+
if (!itemsResult.success) return itemsResult;
|
|
1401
|
+
return ok(transformMatchItems(itemsResult.data, query.season, competition));
|
|
1402
|
+
}
|
|
1403
|
+
case "footywire": {
|
|
1404
|
+
const client = new FootyWireClient();
|
|
1405
|
+
const result = await client.fetchSeasonResults(query.season);
|
|
1406
|
+
if (!result.success) return result;
|
|
1407
|
+
if (query.round != null) {
|
|
1408
|
+
return ok(result.data.filter((m) => m.roundNumber === query.round));
|
|
1409
|
+
}
|
|
1410
|
+
return result;
|
|
1411
|
+
}
|
|
1412
|
+
case "afl-tables": {
|
|
1413
|
+
const client = new AflTablesClient();
|
|
1414
|
+
const result = await client.fetchSeasonResults(query.season);
|
|
1415
|
+
if (!result.success) return result;
|
|
1416
|
+
if (query.round != null) {
|
|
1417
|
+
return ok(result.data.filter((m) => m.roundNumber === query.round));
|
|
1418
|
+
}
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1421
|
+
default:
|
|
1422
|
+
return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/transforms/player-stats.ts
|
|
1427
|
+
function toNullable(value) {
|
|
1428
|
+
return value ?? null;
|
|
1429
|
+
}
|
|
1430
|
+
function transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap) {
|
|
1431
|
+
const inner = item.player.player.player;
|
|
1432
|
+
const stats = item.playerStats.stats;
|
|
1433
|
+
const clearances = stats.clearances;
|
|
1434
|
+
return {
|
|
1435
|
+
matchId,
|
|
1436
|
+
season,
|
|
1437
|
+
roundNumber,
|
|
1438
|
+
team: normaliseTeamName(teamIdMap?.get(item.teamId) ?? item.teamId),
|
|
1439
|
+
competition,
|
|
1440
|
+
playerId: inner.playerId,
|
|
1441
|
+
givenName: inner.playerName.givenName,
|
|
1442
|
+
surname: inner.playerName.surname,
|
|
1443
|
+
displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
|
|
1444
|
+
jumperNumber: item.player.jumperNumber ?? null,
|
|
1445
|
+
kicks: toNullable(stats.kicks),
|
|
1446
|
+
handballs: toNullable(stats.handballs),
|
|
1447
|
+
disposals: toNullable(stats.disposals),
|
|
1448
|
+
marks: toNullable(stats.marks),
|
|
1449
|
+
goals: toNullable(stats.goals),
|
|
1450
|
+
behinds: toNullable(stats.behinds),
|
|
1451
|
+
tackles: toNullable(stats.tackles),
|
|
1452
|
+
hitouts: toNullable(stats.hitouts),
|
|
1453
|
+
freesFor: toNullable(stats.freesFor),
|
|
1454
|
+
freesAgainst: toNullable(stats.freesAgainst),
|
|
1455
|
+
contestedPossessions: toNullable(stats.contestedPossessions),
|
|
1456
|
+
uncontestedPossessions: toNullable(stats.uncontestedPossessions),
|
|
1457
|
+
contestedMarks: toNullable(stats.contestedMarks),
|
|
1458
|
+
intercepts: toNullable(stats.intercepts),
|
|
1459
|
+
centreClearances: toNullable(clearances?.centreClearances),
|
|
1460
|
+
stoppageClearances: toNullable(clearances?.stoppageClearances),
|
|
1461
|
+
totalClearances: toNullable(clearances?.totalClearances),
|
|
1462
|
+
inside50s: toNullable(stats.inside50s),
|
|
1463
|
+
rebound50s: toNullable(stats.rebound50s),
|
|
1464
|
+
clangers: toNullable(stats.clangers),
|
|
1465
|
+
turnovers: toNullable(stats.turnovers),
|
|
1466
|
+
onePercenters: toNullable(stats.onePercenters),
|
|
1467
|
+
bounces: toNullable(stats.bounces),
|
|
1468
|
+
goalAssists: toNullable(stats.goalAssists),
|
|
1469
|
+
disposalEfficiency: toNullable(stats.disposalEfficiency),
|
|
1470
|
+
metresGained: toNullable(stats.metresGained),
|
|
1471
|
+
goalAccuracy: toNullable(stats.goalAccuracy),
|
|
1472
|
+
marksInside50: toNullable(stats.marksInside50),
|
|
1473
|
+
tacklesInside50: toNullable(stats.tacklesInside50),
|
|
1474
|
+
shotsAtGoal: toNullable(stats.shotsAtGoal),
|
|
1475
|
+
scoreInvolvements: toNullable(stats.scoreInvolvements),
|
|
1476
|
+
totalPossessions: toNullable(stats.totalPossessions),
|
|
1477
|
+
timeOnGroundPercentage: toNullable(item.playerStats.timeOnGroundPercentage),
|
|
1478
|
+
ratingPoints: toNullable(stats.ratingPoints),
|
|
1479
|
+
dreamTeamPoints: toNullable(stats.dreamTeamPoints),
|
|
1480
|
+
effectiveDisposals: toNullable(stats.extendedStats?.effectiveDisposals),
|
|
1481
|
+
effectiveKicks: toNullable(stats.extendedStats?.effectiveKicks),
|
|
1482
|
+
kickEfficiency: toNullable(stats.extendedStats?.kickEfficiency),
|
|
1483
|
+
kickToHandballRatio: toNullable(stats.extendedStats?.kickToHandballRatio),
|
|
1484
|
+
pressureActs: toNullable(stats.extendedStats?.pressureActs),
|
|
1485
|
+
defHalfPressureActs: toNullable(stats.extendedStats?.defHalfPressureActs),
|
|
1486
|
+
spoils: toNullable(stats.extendedStats?.spoils),
|
|
1487
|
+
hitoutsToAdvantage: toNullable(stats.extendedStats?.hitoutsToAdvantage),
|
|
1488
|
+
hitoutWinPercentage: toNullable(stats.extendedStats?.hitoutWinPercentage),
|
|
1489
|
+
hitoutToAdvantageRate: toNullable(stats.extendedStats?.hitoutToAdvantageRate),
|
|
1490
|
+
groundBallGets: toNullable(stats.extendedStats?.groundBallGets),
|
|
1491
|
+
f50GroundBallGets: toNullable(stats.extendedStats?.f50GroundBallGets),
|
|
1492
|
+
interceptMarks: toNullable(stats.extendedStats?.interceptMarks),
|
|
1493
|
+
marksOnLead: toNullable(stats.extendedStats?.marksOnLead),
|
|
1494
|
+
contestedPossessionRate: toNullable(stats.extendedStats?.contestedPossessionRate),
|
|
1495
|
+
contestOffOneOnOnes: toNullable(stats.extendedStats?.contestOffOneOnOnes),
|
|
1496
|
+
contestOffWins: toNullable(stats.extendedStats?.contestOffWins),
|
|
1497
|
+
contestOffWinsPercentage: toNullable(stats.extendedStats?.contestOffWinsPercentage),
|
|
1498
|
+
contestDefOneOnOnes: toNullable(stats.extendedStats?.contestDefOneOnOnes),
|
|
1499
|
+
contestDefLosses: toNullable(stats.extendedStats?.contestDefLosses),
|
|
1500
|
+
contestDefLossPercentage: toNullable(stats.extendedStats?.contestDefLossPercentage),
|
|
1501
|
+
centreBounceAttendances: toNullable(stats.extendedStats?.centreBounceAttendances),
|
|
1502
|
+
kickins: toNullable(stats.extendedStats?.kickins),
|
|
1503
|
+
kickinsPlayon: toNullable(stats.extendedStats?.kickinsPlayon),
|
|
1504
|
+
ruckContests: toNullable(stats.extendedStats?.ruckContests),
|
|
1505
|
+
scoreLaunches: toNullable(stats.extendedStats?.scoreLaunches),
|
|
1506
|
+
source
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
function transformPlayerStats(data, matchId, season, roundNumber, competition, source = "afl-api", teamIdMap) {
|
|
1510
|
+
const home = data.homeTeamPlayerStats.map(
|
|
1511
|
+
(item) => transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap)
|
|
1512
|
+
);
|
|
1513
|
+
const away = data.awayTeamPlayerStats.map(
|
|
1514
|
+
(item) => transformOne(item, matchId, season, roundNumber, competition, source, teamIdMap)
|
|
1515
|
+
);
|
|
1516
|
+
return [...home, ...away];
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/api/player-stats.ts
|
|
1520
|
+
async function fetchPlayerStats(query) {
|
|
1521
|
+
const competition = query.competition ?? "AFLM";
|
|
1522
|
+
switch (query.source) {
|
|
1523
|
+
case "afl-api": {
|
|
1524
|
+
const client = new AflApiClient();
|
|
1525
|
+
if (query.matchId) {
|
|
1526
|
+
const result = await client.fetchPlayerStats(query.matchId);
|
|
1527
|
+
if (!result.success) return result;
|
|
1528
|
+
return ok(
|
|
1529
|
+
transformPlayerStats(
|
|
1530
|
+
result.data,
|
|
1531
|
+
query.matchId,
|
|
1532
|
+
query.season,
|
|
1533
|
+
query.round ?? 0,
|
|
1534
|
+
competition
|
|
1535
|
+
)
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
1539
|
+
if (!seasonResult.success) return seasonResult;
|
|
1540
|
+
const roundNumber = query.round ?? 1;
|
|
1541
|
+
const matchItemsResult = await client.fetchRoundMatchItemsByNumber(
|
|
1542
|
+
seasonResult.data,
|
|
1543
|
+
roundNumber
|
|
1544
|
+
);
|
|
1545
|
+
if (!matchItemsResult.success) return matchItemsResult;
|
|
1546
|
+
const teamIdMap = /* @__PURE__ */ new Map();
|
|
1547
|
+
for (const item of matchItemsResult.data) {
|
|
1548
|
+
teamIdMap.set(item.match.homeTeamId, item.match.homeTeam.name);
|
|
1549
|
+
teamIdMap.set(item.match.awayTeamId, item.match.awayTeam.name);
|
|
1550
|
+
}
|
|
1551
|
+
const statsResults = await Promise.all(
|
|
1552
|
+
matchItemsResult.data.map((item) => client.fetchPlayerStats(item.match.matchId))
|
|
1553
|
+
);
|
|
1554
|
+
const allStats = [];
|
|
1555
|
+
for (let i = 0; i < statsResults.length; i++) {
|
|
1556
|
+
const statsResult = statsResults[i];
|
|
1557
|
+
if (!statsResult?.success)
|
|
1558
|
+
return statsResult ?? err(new AflApiError("Missing stats result"));
|
|
1559
|
+
const item = matchItemsResult.data[i];
|
|
1560
|
+
if (!item) continue;
|
|
1561
|
+
allStats.push(
|
|
1562
|
+
...transformPlayerStats(
|
|
1563
|
+
statsResult.data,
|
|
1564
|
+
item.match.matchId,
|
|
1565
|
+
query.season,
|
|
1566
|
+
roundNumber,
|
|
1567
|
+
competition,
|
|
1568
|
+
"afl-api",
|
|
1569
|
+
teamIdMap
|
|
1570
|
+
)
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
return ok(allStats);
|
|
1574
|
+
}
|
|
1575
|
+
case "footywire":
|
|
1576
|
+
return err(
|
|
1577
|
+
new UnsupportedSourceError(
|
|
1578
|
+
"Player stats from FootyWire are not yet supported. Use source: 'afl-api'.",
|
|
1579
|
+
"footywire"
|
|
1580
|
+
)
|
|
1581
|
+
);
|
|
1582
|
+
case "afl-tables":
|
|
1583
|
+
return err(
|
|
1584
|
+
new UnsupportedSourceError(
|
|
1585
|
+
"Player stats from AFL Tables are not yet supported. Use source: 'afl-api'.",
|
|
1586
|
+
"afl-tables"
|
|
1587
|
+
)
|
|
1588
|
+
);
|
|
1589
|
+
default:
|
|
1590
|
+
return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/api/teams.ts
|
|
1595
|
+
function teamTypeForComp(comp) {
|
|
1596
|
+
return comp === "AFLW" ? "WOMEN" : "MEN";
|
|
1597
|
+
}
|
|
1598
|
+
async function fetchTeams(query) {
|
|
1599
|
+
const client = new AflApiClient();
|
|
1600
|
+
const teamType = query?.teamType ?? (query?.competition ? teamTypeForComp(query.competition) : void 0);
|
|
1601
|
+
const result = await client.fetchTeams(teamType);
|
|
1602
|
+
if (!result.success) return result;
|
|
1603
|
+
const competition = query?.competition ?? "AFLM";
|
|
1604
|
+
return ok(
|
|
1605
|
+
result.data.map((t) => ({
|
|
1606
|
+
teamId: String(t.id),
|
|
1607
|
+
name: normaliseTeamName(t.name),
|
|
1608
|
+
abbreviation: t.abbreviation ?? "",
|
|
1609
|
+
competition
|
|
1610
|
+
}))
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
async function fetchSquad(query) {
|
|
1614
|
+
const client = new AflApiClient();
|
|
1615
|
+
const competition = query.competition ?? "AFLM";
|
|
1616
|
+
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
1617
|
+
if (!seasonResult.success) return seasonResult;
|
|
1618
|
+
const teamId = Number.parseInt(query.teamId, 10);
|
|
1619
|
+
if (Number.isNaN(teamId)) {
|
|
1620
|
+
return err(new ValidationError(`Invalid team ID: ${query.teamId}`));
|
|
1621
|
+
}
|
|
1622
|
+
const squadResult = await client.fetchSquad(teamId, seasonResult.data);
|
|
1623
|
+
if (!squadResult.success) return squadResult;
|
|
1624
|
+
const players = squadResult.data.squad.players.map((p) => ({
|
|
1625
|
+
playerId: p.player.providerId ?? String(p.player.id),
|
|
1626
|
+
givenName: p.player.firstName,
|
|
1627
|
+
surname: p.player.surname,
|
|
1628
|
+
displayName: `${p.player.firstName} ${p.player.surname}`,
|
|
1629
|
+
jumperNumber: p.jumperNumber ?? null,
|
|
1630
|
+
position: p.position ?? null,
|
|
1631
|
+
dateOfBirth: p.player.dateOfBirth ? new Date(p.player.dateOfBirth) : null,
|
|
1632
|
+
heightCm: p.player.heightInCm ?? null,
|
|
1633
|
+
weightKg: p.player.weightInKg ?? null,
|
|
1634
|
+
draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
|
|
1635
|
+
draftPosition: p.player.draftPosition ? Number.parseInt(p.player.draftPosition, 10) || null : null,
|
|
1636
|
+
draftType: p.player.draftType ?? null,
|
|
1637
|
+
debutYear: p.player.debutYear ? Number.parseInt(p.player.debutYear, 10) || null : null,
|
|
1638
|
+
recruitedFrom: p.player.recruitedFrom ?? null
|
|
1639
|
+
}));
|
|
1640
|
+
return ok({
|
|
1641
|
+
teamId: query.teamId,
|
|
1642
|
+
teamName: normaliseTeamName(squadResult.data.squad.team?.name ?? query.teamId),
|
|
1643
|
+
season: query.season,
|
|
1644
|
+
players,
|
|
1645
|
+
competition
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// src/sources/fryzigg.ts
|
|
1650
|
+
function fetchFryziggStats() {
|
|
1651
|
+
return err(
|
|
1652
|
+
new ScrapeError(
|
|
1653
|
+
"Fryzigg data is only available in R-specific RDS binary format and cannot be consumed from TypeScript. Use the AFL API source for player statistics instead.",
|
|
1654
|
+
"fryzigg"
|
|
1655
|
+
)
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
export {
|
|
2
1659
|
AflApiClient,
|
|
3
1660
|
AflApiError,
|
|
4
1661
|
AflApiTokenSchema,
|
|
@@ -56,64 +1713,4 @@ import {
|
|
|
56
1713
|
transformMatchItems,
|
|
57
1714
|
transformMatchRoster,
|
|
58
1715
|
transformPlayerStats
|
|
59
|
-
} from "./shared/chunk-eyrvakjt.js";
|
|
60
|
-
import"./shared/chunk-xv8z2kms.js";
|
|
61
|
-
export {
|
|
62
|
-
transformPlayerStats,
|
|
63
|
-
transformMatchRoster,
|
|
64
|
-
transformMatchItems,
|
|
65
|
-
transformLadderEntries,
|
|
66
|
-
toAestString,
|
|
67
|
-
parseFootyWireDate,
|
|
68
|
-
parseAflTablesDate,
|
|
69
|
-
parseAflApiDate,
|
|
70
|
-
ok,
|
|
71
|
-
normaliseTeamName,
|
|
72
|
-
inferRoundType,
|
|
73
|
-
fetchTeams,
|
|
74
|
-
fetchSquad,
|
|
75
|
-
fetchPlayerStats,
|
|
76
|
-
fetchMatchResults,
|
|
77
|
-
fetchLineup,
|
|
78
|
-
fetchLadder,
|
|
79
|
-
fetchFryziggStats,
|
|
80
|
-
fetchFixture,
|
|
81
|
-
err,
|
|
82
|
-
ValidationError,
|
|
83
|
-
UnsupportedSourceError,
|
|
84
|
-
TeamScoreSchema,
|
|
85
|
-
TeamPlayersSchema,
|
|
86
|
-
TeamListSchema,
|
|
87
|
-
TeamItemSchema,
|
|
88
|
-
SquadSchema,
|
|
89
|
-
SquadPlayerItemSchema,
|
|
90
|
-
SquadPlayerInnerSchema,
|
|
91
|
-
SquadListSchema,
|
|
92
|
-
ScrapeError,
|
|
93
|
-
ScoreSchema,
|
|
94
|
-
RoundSchema,
|
|
95
|
-
RoundListSchema,
|
|
96
|
-
RosterPlayerSchema,
|
|
97
|
-
PlayerStatsListSchema,
|
|
98
|
-
PlayerStatsItemSchema,
|
|
99
|
-
PlayerGameStatsSchema,
|
|
100
|
-
PeriodScoreSchema,
|
|
101
|
-
MatchRosterSchema,
|
|
102
|
-
MatchItemSchema,
|
|
103
|
-
MatchItemListSchema,
|
|
104
|
-
LadderResponseSchema,
|
|
105
|
-
LadderEntryRawSchema,
|
|
106
|
-
FootyWireClient,
|
|
107
|
-
CompseasonSchema,
|
|
108
|
-
CompseasonListSchema,
|
|
109
|
-
CompetitionSchema,
|
|
110
|
-
CompetitionListSchema,
|
|
111
|
-
CfsVenueSchema,
|
|
112
|
-
CfsScoreSchema,
|
|
113
|
-
CfsMatchTeamSchema,
|
|
114
|
-
CfsMatchSchema,
|
|
115
|
-
AflTablesClient,
|
|
116
|
-
AflApiTokenSchema,
|
|
117
|
-
AflApiError,
|
|
118
|
-
AflApiClient
|
|
119
1716
|
};
|