fitzroy 1.7.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -17
- package/dist/cli.js +4579 -4241
- package/dist/index.d.ts +177 -120
- package/dist/index.js +2076 -1744
- package/package.json +1 -2
package/dist/index.js
CHANGED
|
@@ -20,12 +20,25 @@ var UnsupportedSourceError = class extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
name = "UnsupportedSourceError";
|
|
22
22
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
source
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
var UnsupportedCompetitionError = class extends Error {
|
|
24
|
+
constructor(message, source, competition, suggestion) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.source = source;
|
|
27
|
+
this.competition = competition;
|
|
28
|
+
this.suggestion = suggestion;
|
|
29
|
+
}
|
|
30
|
+
name = "UnsupportedCompetitionError";
|
|
31
|
+
};
|
|
32
|
+
var OutOfRangeError = class extends Error {
|
|
33
|
+
constructor(message, source, competition, season, suggestion) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.source = source;
|
|
36
|
+
this.competition = competition;
|
|
37
|
+
this.season = season;
|
|
38
|
+
this.suggestion = suggestion;
|
|
39
|
+
}
|
|
40
|
+
name = "OutOfRangeError";
|
|
41
|
+
};
|
|
29
42
|
var ValidationError = class extends Error {
|
|
30
43
|
constructor(message, issues) {
|
|
31
44
|
super(message);
|
|
@@ -41,142 +54,39 @@ function ok(data) {
|
|
|
41
54
|
function err(error) {
|
|
42
55
|
return { success: false, error };
|
|
43
56
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
const mdyMatch = /^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})$/.exec(normalised);
|
|
72
|
-
if (mdyMatch) {
|
|
73
|
-
const [, monthStr, dayStr, yearStr] = mdyMatch;
|
|
74
|
-
if (dayStr && monthStr && yearStr) {
|
|
75
|
-
return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const shortMatch = /^(\d{1,2})\s+([A-Za-z]+)(?:\s+(\d{1,2}):(\d{2})(am|pm))?$/i.exec(normalised);
|
|
79
|
-
if (shortMatch && defaultYear != null) {
|
|
80
|
-
const [, dayStr, monthStr, hourStr, minStr, ampm] = shortMatch;
|
|
81
|
-
if (!dayStr || !monthStr) return null;
|
|
82
|
-
const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
|
|
83
|
-
if (monthIndex === void 0) return null;
|
|
84
|
-
const day = Number.parseInt(dayStr, 10);
|
|
85
|
-
if (!hourStr || !minStr || !ampm) {
|
|
86
|
-
return buildUtcDate(defaultYear, monthStr, day);
|
|
57
|
+
var Result = {
|
|
58
|
+
/** Transform the success value of a Result. Errors pass through unchanged. */
|
|
59
|
+
map(result, fn) {
|
|
60
|
+
return result.success ? ok(fn(result.data)) : result;
|
|
61
|
+
},
|
|
62
|
+
/** Chain a Result-returning function. Errors short-circuit. */
|
|
63
|
+
flatMap(result, fn) {
|
|
64
|
+
return result.success ? fn(result.data) : result;
|
|
65
|
+
},
|
|
66
|
+
/**
|
|
67
|
+
* Chain an async Result-returning function. Errors short-circuit without
|
|
68
|
+
* invoking `fn`.
|
|
69
|
+
*/
|
|
70
|
+
async flatMapAsync(result, fn) {
|
|
71
|
+
return result.success ? fn(result.data) : result;
|
|
72
|
+
},
|
|
73
|
+
/**
|
|
74
|
+
* Collect an array of Results into a single Result of an array. Returns
|
|
75
|
+
* the first error encountered, or `ok` of all successful values.
|
|
76
|
+
*/
|
|
77
|
+
all(results) {
|
|
78
|
+
const data = [];
|
|
79
|
+
for (const r of results) {
|
|
80
|
+
if (!r.success) return r;
|
|
81
|
+
data.push(r.data);
|
|
87
82
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return Number.isNaN(date.getTime()) ? null : date;
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
function parseAflApiDate(iso) {
|
|
98
|
-
return parseDate(iso);
|
|
99
|
-
}
|
|
100
|
-
function parseAflApiMatchTime(iso) {
|
|
101
|
-
return parseDate(iso);
|
|
102
|
-
}
|
|
103
|
-
function parseFootyWireDate(dateStr, defaultYear) {
|
|
104
|
-
return parseDate(dateStr, defaultYear);
|
|
105
|
-
}
|
|
106
|
-
function parseAflTablesDate(dateStr) {
|
|
107
|
-
return parseDate(dateStr);
|
|
108
|
-
}
|
|
109
|
-
function toAestString(date) {
|
|
110
|
-
const formatter = new Intl.DateTimeFormat("en-AU", {
|
|
111
|
-
timeZone: "Australia/Melbourne",
|
|
112
|
-
weekday: "short",
|
|
113
|
-
day: "numeric",
|
|
114
|
-
month: "short",
|
|
115
|
-
year: "numeric",
|
|
116
|
-
hour: "numeric",
|
|
117
|
-
minute: "2-digit",
|
|
118
|
-
hour12: true,
|
|
119
|
-
timeZoneName: "short"
|
|
120
|
-
});
|
|
121
|
-
return formatter.format(date);
|
|
122
|
-
}
|
|
123
|
-
function resolveDefaultSeason(competition = "AFLM") {
|
|
124
|
-
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
125
|
-
return competition === "AFLW" ? year - 1 : year;
|
|
126
|
-
}
|
|
127
|
-
var MONTH_ABBREV_TO_INDEX = /* @__PURE__ */ new Map([
|
|
128
|
-
["jan", 0],
|
|
129
|
-
["feb", 1],
|
|
130
|
-
["mar", 2],
|
|
131
|
-
["apr", 3],
|
|
132
|
-
["may", 4],
|
|
133
|
-
["jun", 5],
|
|
134
|
-
["jul", 6],
|
|
135
|
-
["aug", 7],
|
|
136
|
-
["sep", 8],
|
|
137
|
-
["oct", 9],
|
|
138
|
-
["nov", 10],
|
|
139
|
-
["dec", 11],
|
|
140
|
-
["january", 0],
|
|
141
|
-
["february", 1],
|
|
142
|
-
["march", 2],
|
|
143
|
-
["april", 3],
|
|
144
|
-
["june", 5],
|
|
145
|
-
["july", 6],
|
|
146
|
-
["august", 7],
|
|
147
|
-
["september", 8],
|
|
148
|
-
["october", 9],
|
|
149
|
-
["november", 10],
|
|
150
|
-
["december", 11]
|
|
151
|
-
]);
|
|
152
|
-
function melbourneLocalToUtc(year, monthIndex, day, hours, minutes) {
|
|
153
|
-
const aestGuess = new Date(Date.UTC(year, monthIndex, day, hours - 10, minutes));
|
|
154
|
-
const parts = new Intl.DateTimeFormat("en-AU", {
|
|
155
|
-
timeZone: "Australia/Melbourne",
|
|
156
|
-
day: "2-digit",
|
|
157
|
-
hour: "2-digit",
|
|
158
|
-
hour12: false
|
|
159
|
-
}).formatToParts(aestGuess);
|
|
160
|
-
const getNum = (type) => Number(parts.find((p) => p.type === type)?.value);
|
|
161
|
-
if (getNum("day") === day && getNum("hour") === hours % 24) {
|
|
162
|
-
return aestGuess;
|
|
163
|
-
}
|
|
164
|
-
return new Date(Date.UTC(year, monthIndex, day, hours - 11, minutes));
|
|
165
|
-
}
|
|
166
|
-
function buildUtcDate(year, monthStr, day) {
|
|
167
|
-
const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
|
|
168
|
-
if (monthIndex === void 0) {
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
const date = new Date(Date.UTC(year, monthIndex, day));
|
|
172
|
-
if (Number.isNaN(date.getTime())) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
|
|
176
|
-
return null;
|
|
83
|
+
return ok(data);
|
|
84
|
+
},
|
|
85
|
+
/** Transform the error value of a Result. Successes pass through unchanged. */
|
|
86
|
+
mapErr(result, fn) {
|
|
87
|
+
return result.success ? result : err(fn(result.error));
|
|
177
88
|
}
|
|
178
|
-
|
|
179
|
-
}
|
|
89
|
+
};
|
|
180
90
|
|
|
181
91
|
// src/lib/team-mapping.ts
|
|
182
92
|
var TEAM_ALIASES = [
|
|
@@ -273,85 +183,374 @@ var AFL_API_TEAM_IDS = /* @__PURE__ */ new Map([
|
|
|
273
183
|
["CD_T140", "Western Bulldogs"]
|
|
274
184
|
]);
|
|
275
185
|
|
|
276
|
-
// src/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
["Blacktown International Sportspark"],
|
|
308
|
-
["Barossa Park", "Barossa Oval", "Adelaide Hills"],
|
|
309
|
-
["Ninja Stadium", "Summit Sports Park"]
|
|
310
|
-
];
|
|
311
|
-
var VENUE_ALIAS_MAP = (() => {
|
|
312
|
-
const map = /* @__PURE__ */ new Map();
|
|
313
|
-
for (const [canonical, ...aliases] of VENUE_ALIASES) {
|
|
314
|
-
map.set(canonical.toLowerCase(), canonical);
|
|
315
|
-
for (const alias of aliases) {
|
|
316
|
-
map.set(alias.toLowerCase(), canonical);
|
|
186
|
+
// src/sources/afl-coaches.ts
|
|
187
|
+
import * as cheerio from "cheerio";
|
|
188
|
+
var AflCoachesClient = class {
|
|
189
|
+
fetchFn;
|
|
190
|
+
constructor(options) {
|
|
191
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Fetch the HTML content of an AFLCA page.
|
|
195
|
+
*/
|
|
196
|
+
async fetchHtml(url) {
|
|
197
|
+
try {
|
|
198
|
+
const response = await this.fetchFn(url, {
|
|
199
|
+
headers: {
|
|
200
|
+
"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"
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
return err(
|
|
205
|
+
new ScrapeError(`AFL Coaches request failed: ${response.status} (${url})`, "afl-coaches")
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const html = await response.text();
|
|
209
|
+
return ok(html);
|
|
210
|
+
} catch (cause) {
|
|
211
|
+
return err(
|
|
212
|
+
new ScrapeError(
|
|
213
|
+
`AFL Coaches request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
214
|
+
"afl-coaches"
|
|
215
|
+
)
|
|
216
|
+
);
|
|
317
217
|
}
|
|
318
218
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Build the AFLCA leaderboard URL for a given season, round, and competition.
|
|
221
|
+
*
|
|
222
|
+
* Mirrors the R package URL construction from `helper-aflcoaches.R`.
|
|
223
|
+
*
|
|
224
|
+
* @param season - Season year (e.g. 2024).
|
|
225
|
+
* @param roundNumber - Round number.
|
|
226
|
+
* @param competition - "AFLM" or "AFLW".
|
|
227
|
+
* @param isFinals - Whether this is a finals round.
|
|
228
|
+
*/
|
|
229
|
+
buildUrl(season, roundNumber, competition, isFinals) {
|
|
230
|
+
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/";
|
|
231
|
+
const compSuffix = competition === "AFLW" ? "02" : "01";
|
|
232
|
+
const secondPart = season >= 2023 ? season + 1 : season;
|
|
233
|
+
const roundPad = String(roundNumber).padStart(2, "0");
|
|
234
|
+
return `${linkBase}${season}/${secondPart}${compSuffix}${roundPad}`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Scrape coaches votes for a single round.
|
|
238
|
+
*
|
|
239
|
+
* @param season - Season year.
|
|
240
|
+
* @param roundNumber - Round number.
|
|
241
|
+
* @param competition - "AFLM" or "AFLW".
|
|
242
|
+
* @param isFinals - Whether this is a finals round.
|
|
243
|
+
* @returns Array of coaches vote records for that round.
|
|
244
|
+
*/
|
|
245
|
+
async scrapeRoundVotes(season, roundNumber, competition, isFinals) {
|
|
246
|
+
const url = this.buildUrl(season, roundNumber, competition, isFinals);
|
|
247
|
+
const htmlResult = await this.fetchHtml(url);
|
|
248
|
+
if (!htmlResult.success) {
|
|
249
|
+
return htmlResult;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const votes = parseCoachesVotesHtml(htmlResult.data, season, roundNumber);
|
|
253
|
+
return ok(votes);
|
|
254
|
+
} catch (cause) {
|
|
255
|
+
return err(
|
|
256
|
+
new ScrapeError(
|
|
257
|
+
`Failed to parse coaches votes: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
258
|
+
"afl-coaches"
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Fetch coaches votes for an entire season (all rounds).
|
|
265
|
+
*
|
|
266
|
+
* Iterates over rounds 1-30, skipping rounds that return errors (e.g. byes or
|
|
267
|
+
* rounds that haven't been played yet). Finals rounds (>= 24) use the finals URL.
|
|
268
|
+
*
|
|
269
|
+
* @param season - Season year.
|
|
270
|
+
* @param competition - "AFLM" or "AFLW".
|
|
271
|
+
* @returns Combined array of coaches votes for the season.
|
|
272
|
+
*/
|
|
273
|
+
async fetchSeasonVotes(season, competition) {
|
|
274
|
+
const allVotes = [];
|
|
275
|
+
const maxRound = 30;
|
|
276
|
+
for (let round = 1; round <= maxRound; round++) {
|
|
277
|
+
const isFinals = round >= 24 && season >= 2018;
|
|
278
|
+
const result = await this.scrapeRoundVotes(season, round, competition, isFinals);
|
|
279
|
+
if (result.success && result.data.length > 0) {
|
|
280
|
+
allVotes.push(...result.data);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (allVotes.length === 0) {
|
|
284
|
+
return err(new ScrapeError(`No coaches votes found for season ${season}`, "afl-coaches"));
|
|
285
|
+
}
|
|
286
|
+
return ok(allVotes);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
function parseCoachesVotesHtml(html, season, roundNumber) {
|
|
290
|
+
const $ = cheerio.load(html);
|
|
291
|
+
const clubLogos = $(".pr-md-3.votes-by-match .club_logo");
|
|
292
|
+
const homeTeams = [];
|
|
293
|
+
const awayTeams = [];
|
|
294
|
+
clubLogos.each((i, el) => {
|
|
295
|
+
const title = $(el).attr("title") ?? "";
|
|
296
|
+
if (i % 2 === 0) {
|
|
297
|
+
homeTeams.push(title);
|
|
298
|
+
} else {
|
|
299
|
+
awayTeams.push(title);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
const rawVotes = [];
|
|
303
|
+
$(".pr-md-3.votes-by-match .col-2").each((_i, el) => {
|
|
304
|
+
const text = $(el).text().replace(/\n/g, "").replace(/\t/g, "").trim();
|
|
305
|
+
rawVotes.push(text);
|
|
306
|
+
});
|
|
307
|
+
const rawPlayers = [];
|
|
308
|
+
$(".pr-md-3.votes-by-match .col-10").each((_i, el) => {
|
|
309
|
+
const text = $(el).text().replace(/\n/g, "").replace(/\t/g, "").trim();
|
|
310
|
+
rawPlayers.push(text);
|
|
311
|
+
});
|
|
312
|
+
const votes = [];
|
|
313
|
+
let matchIndex = 0;
|
|
314
|
+
for (let i = 0; i < rawPlayers.length; i++) {
|
|
315
|
+
const playerName = rawPlayers[i] ?? "";
|
|
316
|
+
const voteText = rawVotes[i] ?? "";
|
|
317
|
+
if (playerName === "Player (Club)" && voteText === "Votes") {
|
|
318
|
+
matchIndex++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const homeTeam = homeTeams[matchIndex - 1];
|
|
322
|
+
const awayTeam = awayTeams[matchIndex - 1];
|
|
323
|
+
if (homeTeam == null || awayTeam == null) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const voteCount = Number(voteText);
|
|
327
|
+
if (Number.isNaN(voteCount)) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
votes.push({
|
|
331
|
+
type: "coaches",
|
|
332
|
+
season,
|
|
333
|
+
round: roundNumber,
|
|
334
|
+
homeTeam,
|
|
335
|
+
awayTeam,
|
|
336
|
+
playerName,
|
|
337
|
+
votes: voteCount
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return votes;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/sources/footywire.ts
|
|
344
|
+
import * as cheerio3 from "cheerio";
|
|
345
|
+
|
|
346
|
+
// src/lib/date-utils.ts
|
|
347
|
+
function parseDate(raw, defaultYear) {
|
|
348
|
+
if (typeof raw === "number") {
|
|
349
|
+
const date = new Date(raw * 1e3);
|
|
350
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
351
|
+
}
|
|
352
|
+
const trimmed = raw.trim();
|
|
353
|
+
if (trimmed === "") return null;
|
|
354
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) {
|
|
355
|
+
const stripped = trimmed.replace(/[Zz]$|[+-]\d{2}:\d{2}$/, "");
|
|
356
|
+
const utc = stripped.includes("T") ? `${stripped}Z` : `${stripped}T00:00:00Z`;
|
|
357
|
+
const date = new Date(utc);
|
|
358
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
359
|
+
}
|
|
360
|
+
const withoutDow = trimmed.replace(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s+/i, "");
|
|
361
|
+
const normalised = withoutDow.replace(/[-/]/g, " ");
|
|
362
|
+
const fullMatch = /^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/.exec(normalised);
|
|
363
|
+
if (fullMatch) {
|
|
364
|
+
const [, dayStr, monthStr, yearStr] = fullMatch;
|
|
365
|
+
if (dayStr && monthStr && yearStr) {
|
|
366
|
+
return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const mdyMatch = /^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})$/.exec(normalised);
|
|
370
|
+
if (mdyMatch) {
|
|
371
|
+
const [, monthStr, dayStr, yearStr] = mdyMatch;
|
|
372
|
+
if (dayStr && monthStr && yearStr) {
|
|
373
|
+
return buildUtcDate(Number.parseInt(yearStr, 10), monthStr, Number.parseInt(dayStr, 10));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const shortMatch = /^(\d{1,2})\s+([A-Za-z]+)(?:\s+(\d{1,2}):(\d{2})(am|pm))?$/i.exec(normalised);
|
|
377
|
+
if (shortMatch && defaultYear != null) {
|
|
378
|
+
const [, dayStr, monthStr, hourStr, minStr, ampm] = shortMatch;
|
|
379
|
+
if (!dayStr || !monthStr) return null;
|
|
380
|
+
const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
|
|
381
|
+
if (monthIndex === void 0) return null;
|
|
382
|
+
const day = Number.parseInt(dayStr, 10);
|
|
383
|
+
if (!hourStr || !minStr || !ampm) {
|
|
384
|
+
return buildUtcDate(defaultYear, monthStr, day);
|
|
385
|
+
}
|
|
386
|
+
let hours = Number.parseInt(hourStr, 10);
|
|
387
|
+
const minutes = Number.parseInt(minStr, 10);
|
|
388
|
+
if (ampm.toLowerCase() === "pm" && hours < 12) hours += 12;
|
|
389
|
+
if (ampm.toLowerCase() === "am" && hours === 12) hours = 0;
|
|
390
|
+
const date = melbourneLocalToUtc(defaultYear, monthIndex, day, hours, minutes);
|
|
391
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
function parseAflApiDate(iso) {
|
|
396
|
+
return parseDate(iso);
|
|
397
|
+
}
|
|
398
|
+
function parseAflApiMatchTime(iso) {
|
|
399
|
+
return parseDate(iso);
|
|
400
|
+
}
|
|
401
|
+
function parseFootyWireDate(dateStr, defaultYear) {
|
|
402
|
+
return parseDate(dateStr, defaultYear);
|
|
403
|
+
}
|
|
404
|
+
function parseAflTablesDate(dateStr) {
|
|
405
|
+
return parseDate(dateStr);
|
|
406
|
+
}
|
|
407
|
+
function toAestString(date) {
|
|
408
|
+
const formatter = new Intl.DateTimeFormat("en-AU", {
|
|
409
|
+
timeZone: "Australia/Melbourne",
|
|
410
|
+
weekday: "short",
|
|
411
|
+
day: "numeric",
|
|
412
|
+
month: "short",
|
|
413
|
+
year: "numeric",
|
|
414
|
+
hour: "numeric",
|
|
415
|
+
minute: "2-digit",
|
|
416
|
+
hour12: true,
|
|
417
|
+
timeZoneName: "short"
|
|
418
|
+
});
|
|
419
|
+
return formatter.format(date);
|
|
420
|
+
}
|
|
421
|
+
var MONTH_ABBREV_TO_INDEX = /* @__PURE__ */ new Map([
|
|
422
|
+
["jan", 0],
|
|
423
|
+
["feb", 1],
|
|
424
|
+
["mar", 2],
|
|
425
|
+
["apr", 3],
|
|
426
|
+
["may", 4],
|
|
427
|
+
["jun", 5],
|
|
428
|
+
["jul", 6],
|
|
429
|
+
["aug", 7],
|
|
430
|
+
["sep", 8],
|
|
431
|
+
["oct", 9],
|
|
432
|
+
["nov", 10],
|
|
433
|
+
["dec", 11],
|
|
434
|
+
["january", 0],
|
|
435
|
+
["february", 1],
|
|
436
|
+
["march", 2],
|
|
437
|
+
["april", 3],
|
|
438
|
+
["june", 5],
|
|
439
|
+
["july", 6],
|
|
440
|
+
["august", 7],
|
|
441
|
+
["september", 8],
|
|
442
|
+
["october", 9],
|
|
443
|
+
["november", 10],
|
|
444
|
+
["december", 11]
|
|
445
|
+
]);
|
|
446
|
+
function melbourneLocalToUtc(year, monthIndex, day, hours, minutes) {
|
|
447
|
+
const aestGuess = new Date(Date.UTC(year, monthIndex, day, hours - 10, minutes));
|
|
448
|
+
const parts = new Intl.DateTimeFormat("en-AU", {
|
|
449
|
+
timeZone: "Australia/Melbourne",
|
|
450
|
+
day: "2-digit",
|
|
451
|
+
hour: "2-digit",
|
|
452
|
+
hour12: false
|
|
453
|
+
}).formatToParts(aestGuess);
|
|
454
|
+
const getNum = (type) => Number(parts.find((p) => p.type === type)?.value);
|
|
455
|
+
if (getNum("day") === day && getNum("hour") === hours % 24) {
|
|
456
|
+
return aestGuess;
|
|
457
|
+
}
|
|
458
|
+
return new Date(Date.UTC(year, monthIndex, day, hours - 11, minutes));
|
|
459
|
+
}
|
|
460
|
+
function buildUtcDate(year, monthStr, day) {
|
|
461
|
+
const monthIndex = MONTH_ABBREV_TO_INDEX.get(monthStr.toLowerCase());
|
|
462
|
+
if (monthIndex === void 0) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const date = new Date(Date.UTC(year, monthIndex, day));
|
|
466
|
+
if (Number.isNaN(date.getTime())) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== monthIndex || date.getUTCDate() !== day) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
return date;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/lib/venue-mapping.ts
|
|
476
|
+
var VENUE_ALIASES = [
|
|
477
|
+
["MCG", "M.C.G.", "Melbourne Cricket Ground"],
|
|
478
|
+
["SCG", "S.C.G.", "Sydney Cricket Ground"],
|
|
479
|
+
["Marvel Stadium", "Docklands", "Etihad Stadium", "Telstra Dome", "Colonial Stadium"],
|
|
480
|
+
["Kardinia Park", "GMHBA Stadium", "Simonds Stadium", "Skilled Stadium"],
|
|
481
|
+
["Gabba", "The Gabba", "Brisbane Cricket Ground"],
|
|
482
|
+
[
|
|
483
|
+
"Sydney Showground",
|
|
484
|
+
"ENGIE Stadium",
|
|
485
|
+
"GIANTS Stadium",
|
|
486
|
+
"Showground Stadium",
|
|
487
|
+
"Sydney Showground Stadium"
|
|
488
|
+
],
|
|
489
|
+
["Accor Stadium", "Stadium Australia", "ANZ Stadium", "Homebush"],
|
|
490
|
+
["Carrara", "People First Stadium", "Heritage Bank Stadium", "Metricon Stadium"],
|
|
491
|
+
["Perth Stadium", "Optus Stadium"],
|
|
492
|
+
["Adelaide Oval"],
|
|
493
|
+
["Manuka Oval", "Corroboree Group Oval Manuka"],
|
|
494
|
+
["Blundstone Arena", "Bellerive Oval"],
|
|
495
|
+
["UTAS Stadium", "York Park", "University of Tasmania Stadium", "Aurora Stadium"],
|
|
496
|
+
["TIO Stadium", "Marrara Oval"],
|
|
497
|
+
["Traeger Park", "TIO Traeger Park"],
|
|
498
|
+
["Mars Stadium", "Eureka Stadium"],
|
|
499
|
+
["Cazalys Stadium", "Cazaly's Stadium"],
|
|
500
|
+
["Jiangwan Stadium"],
|
|
501
|
+
["Riverway Stadium"],
|
|
502
|
+
["Norwood Oval"],
|
|
503
|
+
["Subiaco Oval", "Subiaco"],
|
|
504
|
+
["Football Park", "AAMI Stadium"],
|
|
505
|
+
["Princes Park", "Ikon Park"],
|
|
506
|
+
["Blacktown International Sportspark"],
|
|
507
|
+
["Barossa Park", "Barossa Oval", "Adelaide Hills"],
|
|
508
|
+
["Ninja Stadium", "Summit Sports Park"]
|
|
509
|
+
];
|
|
510
|
+
var VENUE_ALIAS_MAP = (() => {
|
|
511
|
+
const map = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const [canonical, ...aliases] of VENUE_ALIASES) {
|
|
513
|
+
map.set(canonical.toLowerCase(), canonical);
|
|
514
|
+
for (const alias of aliases) {
|
|
515
|
+
map.set(alias.toLowerCase(), canonical);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return map;
|
|
519
|
+
})();
|
|
520
|
+
function normaliseVenueName(raw) {
|
|
521
|
+
const trimmed = raw.trim();
|
|
522
|
+
return VENUE_ALIAS_MAP.get(trimmed.toLowerCase()) ?? trimmed;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/transforms/footywire-player-stats.ts
|
|
526
|
+
import * as cheerio2 from "cheerio";
|
|
527
|
+
|
|
528
|
+
// src/lib/parse-utils.ts
|
|
529
|
+
function safeInt(text) {
|
|
530
|
+
const cleaned = text.replace(/[^0-9-]/g, "").trim();
|
|
531
|
+
if (!cleaned) return null;
|
|
532
|
+
const n = Number.parseInt(cleaned, 10);
|
|
533
|
+
return Number.isNaN(n) ? null : n;
|
|
534
|
+
}
|
|
535
|
+
function parseIntOr0(text) {
|
|
536
|
+
const n = Number.parseInt(text.replace(/[^0-9-]/g, ""), 10);
|
|
537
|
+
return Number.isNaN(n) ? 0 : n;
|
|
538
|
+
}
|
|
539
|
+
function parseFloatOr0(text) {
|
|
540
|
+
const n = Number.parseFloat(text.replace(/[^0-9.-]/g, ""));
|
|
541
|
+
return Number.isNaN(n) ? 0 : n;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/transforms/footywire-player-stats.ts
|
|
545
|
+
var BASIC_COLS = [
|
|
546
|
+
"Player",
|
|
547
|
+
"K",
|
|
548
|
+
"HB",
|
|
549
|
+
"D",
|
|
550
|
+
"M",
|
|
551
|
+
"G",
|
|
552
|
+
"B",
|
|
553
|
+
"T",
|
|
355
554
|
"HO",
|
|
356
555
|
"GA",
|
|
357
556
|
"I50",
|
|
@@ -387,7 +586,7 @@ function cleanPlayerName(raw) {
|
|
|
387
586
|
return raw.replace(/[↗↙]/g, "").trim();
|
|
388
587
|
}
|
|
389
588
|
function parseStatsTable(html, expectedCols, rowParser) {
|
|
390
|
-
const $ =
|
|
589
|
+
const $ = cheerio2.load(html);
|
|
391
590
|
const results = [];
|
|
392
591
|
$("table").each((_i, table) => {
|
|
393
592
|
const rows = $(table).find("tr");
|
|
@@ -644,8 +843,9 @@ function transformMatchItems(items, season, competition, source = "afl-api") {
|
|
|
644
843
|
return items.map((item) => {
|
|
645
844
|
const homeScore = item.score?.homeTeamScore;
|
|
646
845
|
const awayScore = item.score?.awayTeamScore;
|
|
647
|
-
const homePoints = homeScore
|
|
648
|
-
const awayPoints = awayScore
|
|
846
|
+
const homePoints = homeScore ? homeScore.matchScore.totalScore : null;
|
|
847
|
+
const awayPoints = awayScore ? awayScore.matchScore.totalScore : null;
|
|
848
|
+
const margin = homePoints !== null && awayPoints !== null ? homePoints - awayPoints : null;
|
|
649
849
|
return {
|
|
650
850
|
matchId: item.match.matchId,
|
|
651
851
|
season,
|
|
@@ -656,13 +856,13 @@ function transformMatchItems(items, season, competition, source = "afl-api") {
|
|
|
656
856
|
venue: item.venue?.name ? normaliseVenueName(item.venue.name) : "",
|
|
657
857
|
homeTeam: normaliseTeamName(item.match.homeTeam.name),
|
|
658
858
|
awayTeam: normaliseTeamName(item.match.awayTeam.name),
|
|
659
|
-
homeGoals: homeScore
|
|
660
|
-
homeBehinds: homeScore
|
|
859
|
+
homeGoals: homeScore ? homeScore.matchScore.goals : null,
|
|
860
|
+
homeBehinds: homeScore ? homeScore.matchScore.behinds : null,
|
|
661
861
|
homePoints,
|
|
662
|
-
awayGoals: awayScore
|
|
663
|
-
awayBehinds: awayScore
|
|
862
|
+
awayGoals: awayScore ? awayScore.matchScore.goals : null,
|
|
863
|
+
awayBehinds: awayScore ? awayScore.matchScore.behinds : null,
|
|
664
864
|
awayPoints,
|
|
665
|
-
margin
|
|
865
|
+
margin,
|
|
666
866
|
q1Home: findPeriod(homeScore?.periodScore, 1),
|
|
667
867
|
q2Home: findPeriod(homeScore?.periodScore, 2),
|
|
668
868
|
q3Home: findPeriod(homeScore?.periodScore, 3),
|
|
@@ -798,7 +998,7 @@ var FootyWireClient = class {
|
|
|
798
998
|
const htmlResult = await this.fetchHtml(url);
|
|
799
999
|
if (!htmlResult.success) return htmlResult;
|
|
800
1000
|
try {
|
|
801
|
-
const $ =
|
|
1001
|
+
const $ = cheerio3.load(htmlResult.data);
|
|
802
1002
|
const entries = [];
|
|
803
1003
|
let currentRound = 0;
|
|
804
1004
|
let lastHARound = 0;
|
|
@@ -924,7 +1124,7 @@ var FootyWireClient = class {
|
|
|
924
1124
|
}
|
|
925
1125
|
};
|
|
926
1126
|
function parseMatchList(html, year) {
|
|
927
|
-
const $ =
|
|
1127
|
+
const $ = cheerio3.load(html);
|
|
928
1128
|
const results = [];
|
|
929
1129
|
let currentRound = 0;
|
|
930
1130
|
let lastHARound = 0;
|
|
@@ -1015,7 +1215,7 @@ function parseMatchList(html, year) {
|
|
|
1015
1215
|
return results;
|
|
1016
1216
|
}
|
|
1017
1217
|
function parseFixtureList(html, year) {
|
|
1018
|
-
const $ =
|
|
1218
|
+
const $ = cheerio3.load(html);
|
|
1019
1219
|
const fixtures = [];
|
|
1020
1220
|
let currentRound = 0;
|
|
1021
1221
|
let lastHARound = 0;
|
|
@@ -1057,18 +1257,45 @@ function parseFixtureList(html, year) {
|
|
|
1057
1257
|
season: year,
|
|
1058
1258
|
roundNumber: currentRound,
|
|
1059
1259
|
roundType: currentRoundType,
|
|
1260
|
+
roundName: null,
|
|
1060
1261
|
date,
|
|
1061
1262
|
venue: normaliseVenueName(venue),
|
|
1062
1263
|
homeTeam,
|
|
1063
1264
|
awayTeam,
|
|
1265
|
+
homeGoals: null,
|
|
1266
|
+
homeBehinds: null,
|
|
1267
|
+
homePoints: null,
|
|
1268
|
+
awayGoals: null,
|
|
1269
|
+
awayBehinds: null,
|
|
1270
|
+
awayPoints: null,
|
|
1271
|
+
margin: null,
|
|
1272
|
+
q1Home: null,
|
|
1273
|
+
q2Home: null,
|
|
1274
|
+
q3Home: null,
|
|
1275
|
+
q4Home: null,
|
|
1276
|
+
q1Away: null,
|
|
1277
|
+
q2Away: null,
|
|
1278
|
+
q3Away: null,
|
|
1279
|
+
q4Away: null,
|
|
1064
1280
|
status: hasScore ? "Complete" : "Upcoming",
|
|
1281
|
+
attendance: null,
|
|
1282
|
+
weatherTempCelsius: null,
|
|
1283
|
+
weatherType: null,
|
|
1284
|
+
roundCode: null,
|
|
1285
|
+
venueState: null,
|
|
1286
|
+
venueTimezone: null,
|
|
1287
|
+
homeRushedBehinds: null,
|
|
1288
|
+
awayRushedBehinds: null,
|
|
1289
|
+
homeMinutesInFront: null,
|
|
1290
|
+
awayMinutesInFront: null,
|
|
1291
|
+
source: "footywire",
|
|
1065
1292
|
competition: "AFLM"
|
|
1066
1293
|
});
|
|
1067
1294
|
});
|
|
1068
1295
|
return fixtures;
|
|
1069
1296
|
}
|
|
1070
1297
|
function parseFootyWireTeamStats(html, year, suffix) {
|
|
1071
|
-
const $ =
|
|
1298
|
+
const $ = cheerio3.load(html);
|
|
1072
1299
|
const entries = [];
|
|
1073
1300
|
const tables = $("table");
|
|
1074
1301
|
const mainTable = tables.length > 10 ? $(tables[10]) : $("table.sortable").first();
|
|
@@ -1162,7 +1389,7 @@ function normaliseDob(raw) {
|
|
|
1162
1389
|
return raw;
|
|
1163
1390
|
}
|
|
1164
1391
|
function parseFootyWirePlayerList(html, teamName) {
|
|
1165
|
-
const $ =
|
|
1392
|
+
const $ = cheerio3.load(html);
|
|
1166
1393
|
const players = [];
|
|
1167
1394
|
let dataRows = null;
|
|
1168
1395
|
$("table").each((_i, table) => {
|
|
@@ -1218,9 +1445,9 @@ function parseFootyWirePlayerList(html, teamName) {
|
|
|
1218
1445
|
}
|
|
1219
1446
|
|
|
1220
1447
|
// src/transforms/awards.ts
|
|
1221
|
-
import * as
|
|
1448
|
+
import * as cheerio4 from "cheerio";
|
|
1222
1449
|
function parseBrownlowVotes(html, season) {
|
|
1223
|
-
const $ =
|
|
1450
|
+
const $ = cheerio4.load(html);
|
|
1224
1451
|
const results = [];
|
|
1225
1452
|
$("table").each((_i, table) => {
|
|
1226
1453
|
const rows = $(table).find("tr");
|
|
@@ -1259,7 +1486,7 @@ function parseBrownlowVotes(html, season) {
|
|
|
1259
1486
|
return results;
|
|
1260
1487
|
}
|
|
1261
1488
|
function parseAllAustralian(html, season) {
|
|
1262
|
-
const $ =
|
|
1489
|
+
const $ = cheerio4.load(html);
|
|
1263
1490
|
const results = [];
|
|
1264
1491
|
const rows = $("tr");
|
|
1265
1492
|
rows.each((_i, row) => {
|
|
@@ -1289,7 +1516,7 @@ function parseAllAustralian(html, season) {
|
|
|
1289
1516
|
return results;
|
|
1290
1517
|
}
|
|
1291
1518
|
function parseRisingStarNominations(html, season) {
|
|
1292
|
-
const $ =
|
|
1519
|
+
const $ = cheerio4.load(html);
|
|
1293
1520
|
const results = [];
|
|
1294
1521
|
const tables = $("table");
|
|
1295
1522
|
let targetRows = null;
|
|
@@ -1334,255 +1561,175 @@ function parseRisingStarNominations(html, season) {
|
|
|
1334
1561
|
return results;
|
|
1335
1562
|
}
|
|
1336
1563
|
|
|
1337
|
-
// src/
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
return err(
|
|
1349
|
-
new ScrapeError(`No Brownlow data found for season ${query.season}`, "footywire")
|
|
1350
|
-
);
|
|
1351
|
-
}
|
|
1352
|
-
return ok(votes);
|
|
1353
|
-
}
|
|
1354
|
-
case "all-australian": {
|
|
1355
|
-
const url = `${FOOTYWIRE_BASE2}/all_australian_selection?year=${query.season}`;
|
|
1356
|
-
const htmlResult = await client.fetchPage(url);
|
|
1357
|
-
if (!htmlResult.success) return htmlResult;
|
|
1358
|
-
const selections = parseAllAustralian(htmlResult.data, query.season);
|
|
1359
|
-
if (selections.length === 0) {
|
|
1360
|
-
return err(
|
|
1361
|
-
new ScrapeError(`No All-Australian data found for season ${query.season}`, "footywire")
|
|
1362
|
-
);
|
|
1363
|
-
}
|
|
1364
|
-
return ok(selections);
|
|
1365
|
-
}
|
|
1366
|
-
case "rising-star": {
|
|
1367
|
-
const url = `${FOOTYWIRE_BASE2}/rising_star_nominations?year=${query.season}`;
|
|
1368
|
-
const htmlResult = await client.fetchPage(url);
|
|
1369
|
-
if (!htmlResult.success) return htmlResult;
|
|
1370
|
-
const nominations = parseRisingStarNominations(htmlResult.data, query.season);
|
|
1371
|
-
if (nominations.length === 0) {
|
|
1372
|
-
return err(
|
|
1373
|
-
new ScrapeError(`No Rising Star data found for season ${query.season}`, "footywire")
|
|
1374
|
-
);
|
|
1375
|
-
}
|
|
1376
|
-
return ok(nominations);
|
|
1564
|
+
// src/lib/concurrency.ts
|
|
1565
|
+
async function batchedMap(items, fn, options) {
|
|
1566
|
+
const batchSize = options?.batchSize ?? 5;
|
|
1567
|
+
const delayMs = options?.delayMs ?? 0;
|
|
1568
|
+
const results = [];
|
|
1569
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
1570
|
+
const batch = items.slice(i, i + batchSize);
|
|
1571
|
+
const batchResults = await Promise.all(batch.map(fn));
|
|
1572
|
+
results.push(...batchResults);
|
|
1573
|
+
if (delayMs > 0 && i + batchSize < items.length) {
|
|
1574
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1377
1575
|
}
|
|
1378
|
-
default:
|
|
1379
|
-
return err(new ScrapeError(`Unknown award type: ${query.award}`, "footywire"));
|
|
1380
1576
|
}
|
|
1577
|
+
return results;
|
|
1381
1578
|
}
|
|
1382
1579
|
|
|
1383
|
-
// src/
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
return err(
|
|
1402
|
-
new ScrapeError(`AFL Coaches request failed: ${response.status} (${url})`, "afl-coaches")
|
|
1403
|
-
);
|
|
1404
|
-
}
|
|
1405
|
-
const html = await response.text();
|
|
1406
|
-
return ok(html);
|
|
1407
|
-
} catch (cause) {
|
|
1408
|
-
return err(
|
|
1409
|
-
new ScrapeError(
|
|
1410
|
-
`AFL Coaches request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1411
|
-
"afl-coaches"
|
|
1412
|
-
)
|
|
1413
|
-
);
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
/**
|
|
1417
|
-
* Build the AFLCA leaderboard URL for a given season, round, and competition.
|
|
1418
|
-
*
|
|
1419
|
-
* Mirrors the R package URL construction from `helper-aflcoaches.R`.
|
|
1420
|
-
*
|
|
1421
|
-
* @param season - Season year (e.g. 2024).
|
|
1422
|
-
* @param roundNumber - Round number.
|
|
1423
|
-
* @param competition - "AFLM" or "AFLW".
|
|
1424
|
-
* @param isFinals - Whether this is a finals round.
|
|
1425
|
-
*/
|
|
1426
|
-
buildUrl(season, roundNumber, competition, isFinals) {
|
|
1427
|
-
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/";
|
|
1428
|
-
const compSuffix = competition === "AFLW" ? "02" : "01";
|
|
1429
|
-
const secondPart = season >= 2023 ? season + 1 : season;
|
|
1430
|
-
const roundPad = String(roundNumber).padStart(2, "0");
|
|
1431
|
-
return `${linkBase}${season}/${secondPart}${compSuffix}${roundPad}`;
|
|
1432
|
-
}
|
|
1433
|
-
/**
|
|
1434
|
-
* Scrape coaches votes for a single round.
|
|
1435
|
-
*
|
|
1436
|
-
* @param season - Season year.
|
|
1437
|
-
* @param roundNumber - Round number.
|
|
1438
|
-
* @param competition - "AFLM" or "AFLW".
|
|
1439
|
-
* @param isFinals - Whether this is a finals round.
|
|
1440
|
-
* @returns Array of coaches vote records for that round.
|
|
1441
|
-
*/
|
|
1442
|
-
async scrapeRoundVotes(season, roundNumber, competition, isFinals) {
|
|
1443
|
-
const url = this.buildUrl(season, roundNumber, competition, isFinals);
|
|
1444
|
-
const htmlResult = await this.fetchHtml(url);
|
|
1445
|
-
if (!htmlResult.success) {
|
|
1446
|
-
return htmlResult;
|
|
1447
|
-
}
|
|
1448
|
-
try {
|
|
1449
|
-
const votes = parseCoachesVotesHtml(htmlResult.data, season, roundNumber);
|
|
1450
|
-
return ok(votes);
|
|
1451
|
-
} catch (cause) {
|
|
1452
|
-
return err(
|
|
1453
|
-
new ScrapeError(
|
|
1454
|
-
`Failed to parse coaches votes: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1455
|
-
"afl-coaches"
|
|
1456
|
-
)
|
|
1457
|
-
);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
/**
|
|
1461
|
-
* Fetch coaches votes for an entire season (all rounds).
|
|
1462
|
-
*
|
|
1463
|
-
* Iterates over rounds 1-30, skipping rounds that return errors (e.g. byes or
|
|
1464
|
-
* rounds that haven't been played yet). Finals rounds (>= 24) use the finals URL.
|
|
1465
|
-
*
|
|
1466
|
-
* @param season - Season year.
|
|
1467
|
-
* @param competition - "AFLM" or "AFLW".
|
|
1468
|
-
* @returns Combined array of coaches votes for the season.
|
|
1469
|
-
*/
|
|
1470
|
-
async fetchSeasonVotes(season, competition) {
|
|
1471
|
-
const allVotes = [];
|
|
1472
|
-
const maxRound = 30;
|
|
1473
|
-
for (let round = 1; round <= maxRound; round++) {
|
|
1474
|
-
const isFinals = round >= 24 && season >= 2018;
|
|
1475
|
-
const result = await this.scrapeRoundVotes(season, round, competition, isFinals);
|
|
1476
|
-
if (result.success && result.data.length > 0) {
|
|
1477
|
-
allVotes.push(...result.data);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
if (allVotes.length === 0) {
|
|
1481
|
-
return err(new ScrapeError(`No coaches votes found for season ${season}`, "afl-coaches"));
|
|
1482
|
-
}
|
|
1483
|
-
return ok(allVotes);
|
|
1484
|
-
}
|
|
1485
|
-
};
|
|
1486
|
-
function parseCoachesVotesHtml(html, season, roundNumber) {
|
|
1487
|
-
const $ = cheerio4.load(html);
|
|
1488
|
-
const clubLogos = $(".pr-md-3.votes-by-match .club_logo");
|
|
1489
|
-
const homeTeams = [];
|
|
1490
|
-
const awayTeams = [];
|
|
1491
|
-
clubLogos.each((i, el) => {
|
|
1492
|
-
const title = $(el).attr("title") ?? "";
|
|
1493
|
-
if (i % 2 === 0) {
|
|
1494
|
-
homeTeams.push(title);
|
|
1495
|
-
} else {
|
|
1496
|
-
awayTeams.push(title);
|
|
1497
|
-
}
|
|
1498
|
-
});
|
|
1499
|
-
const rawVotes = [];
|
|
1500
|
-
$(".pr-md-3.votes-by-match .col-2").each((_i, el) => {
|
|
1501
|
-
const text = $(el).text().replace(/\n/g, "").replace(/\t/g, "").trim();
|
|
1502
|
-
rawVotes.push(text);
|
|
1503
|
-
});
|
|
1504
|
-
const rawPlayers = [];
|
|
1505
|
-
$(".pr-md-3.votes-by-match .col-10").each((_i, el) => {
|
|
1506
|
-
const text = $(el).text().replace(/\n/g, "").replace(/\t/g, "").trim();
|
|
1507
|
-
rawPlayers.push(text);
|
|
1580
|
+
// src/transforms/ladder.ts
|
|
1581
|
+
function transformLadderEntries(entries) {
|
|
1582
|
+
return entries.map((entry) => {
|
|
1583
|
+
const record = entry.thisSeasonRecord;
|
|
1584
|
+
const wl = record?.winLossRecord;
|
|
1585
|
+
return {
|
|
1586
|
+
position: entry.position,
|
|
1587
|
+
team: normaliseTeamName(entry.team.name),
|
|
1588
|
+
played: entry.played ?? wl?.played ?? 0,
|
|
1589
|
+
wins: wl?.wins ?? 0,
|
|
1590
|
+
losses: wl?.losses ?? 0,
|
|
1591
|
+
draws: wl?.draws ?? 0,
|
|
1592
|
+
pointsFor: entry.pointsFor ?? 0,
|
|
1593
|
+
pointsAgainst: entry.pointsAgainst ?? 0,
|
|
1594
|
+
percentage: record?.percentage ?? 0,
|
|
1595
|
+
premiershipsPoints: record?.aggregatePoints ?? 0,
|
|
1596
|
+
form: entry.form ?? null
|
|
1597
|
+
};
|
|
1508
1598
|
});
|
|
1509
|
-
const votes = [];
|
|
1510
|
-
let matchIndex = 0;
|
|
1511
|
-
for (let i = 0; i < rawPlayers.length; i++) {
|
|
1512
|
-
const playerName = rawPlayers[i] ?? "";
|
|
1513
|
-
const voteText = rawVotes[i] ?? "";
|
|
1514
|
-
if (playerName === "Player (Club)" && voteText === "Votes") {
|
|
1515
|
-
matchIndex++;
|
|
1516
|
-
continue;
|
|
1517
|
-
}
|
|
1518
|
-
const homeTeam = homeTeams[matchIndex - 1];
|
|
1519
|
-
const awayTeam = awayTeams[matchIndex - 1];
|
|
1520
|
-
if (homeTeam == null || awayTeam == null) {
|
|
1521
|
-
continue;
|
|
1522
|
-
}
|
|
1523
|
-
const voteCount = Number(voteText);
|
|
1524
|
-
if (Number.isNaN(voteCount)) {
|
|
1525
|
-
continue;
|
|
1526
|
-
}
|
|
1527
|
-
votes.push({
|
|
1528
|
-
season,
|
|
1529
|
-
round: roundNumber,
|
|
1530
|
-
homeTeam,
|
|
1531
|
-
awayTeam,
|
|
1532
|
-
playerName,
|
|
1533
|
-
votes: voteCount
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
return votes;
|
|
1537
1599
|
}
|
|
1538
1600
|
|
|
1539
|
-
// src/
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1601
|
+
// src/transforms/lineup.ts
|
|
1602
|
+
var EMERGENCY_POSITIONS = /* @__PURE__ */ new Set(["EMG", "EMERG"]);
|
|
1603
|
+
var SUBSTITUTE_POSITIONS = /* @__PURE__ */ new Set(["SUB", "INT"]);
|
|
1604
|
+
function transformMatchRoster(roster, season, roundNumber, competition) {
|
|
1605
|
+
const homeTeamId = roster.match.homeTeamId;
|
|
1606
|
+
const awayTeamId = roster.match.awayTeamId;
|
|
1607
|
+
const homeTeamPlayers = roster.teamPlayers.find((tp) => tp.teamId === homeTeamId);
|
|
1608
|
+
const awayTeamPlayers = roster.teamPlayers.find((tp) => tp.teamId === awayTeamId);
|
|
1609
|
+
const mapPlayers = (players) => players.map((p) => {
|
|
1610
|
+
const inner = p.player.player;
|
|
1611
|
+
const position = p.player.position ?? null;
|
|
1612
|
+
return {
|
|
1613
|
+
playerId: inner.playerId,
|
|
1614
|
+
givenName: inner.playerName.givenName,
|
|
1615
|
+
surname: inner.playerName.surname,
|
|
1616
|
+
displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
|
|
1617
|
+
jumperNumber: p.jumperNumber ?? null,
|
|
1618
|
+
position,
|
|
1619
|
+
isEmergency: position !== null && EMERGENCY_POSITIONS.has(position),
|
|
1620
|
+
isSubstitute: position !== null && SUBSTITUTE_POSITIONS.has(position)
|
|
1621
|
+
};
|
|
1622
|
+
});
|
|
1623
|
+
return {
|
|
1624
|
+
matchId: roster.match.matchId,
|
|
1625
|
+
season,
|
|
1626
|
+
roundNumber,
|
|
1627
|
+
homeTeam: normaliseTeamName(roster.match.homeTeam.name),
|
|
1628
|
+
awayTeam: normaliseTeamName(roster.match.awayTeam.name),
|
|
1629
|
+
homePlayers: homeTeamPlayers ? mapPlayers(homeTeamPlayers.players) : [],
|
|
1630
|
+
awayPlayers: awayTeamPlayers ? mapPlayers(awayTeamPlayers.players) : [],
|
|
1631
|
+
competition
|
|
1632
|
+
};
|
|
1570
1633
|
}
|
|
1571
1634
|
|
|
1572
|
-
// src/
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1635
|
+
// src/transforms/player-stats.ts
|
|
1636
|
+
function toNullable(value) {
|
|
1637
|
+
return value ?? null;
|
|
1638
|
+
}
|
|
1639
|
+
function transformOne(item, ctx) {
|
|
1640
|
+
const inner = item.player.player.player;
|
|
1641
|
+
const stats = item.playerStats?.stats;
|
|
1642
|
+
const clearances = stats?.clearances;
|
|
1643
|
+
return {
|
|
1644
|
+
matchId: ctx.matchId,
|
|
1645
|
+
season: ctx.season,
|
|
1646
|
+
roundNumber: ctx.roundNumber,
|
|
1647
|
+
team: normaliseTeamName(
|
|
1648
|
+
ctx.teamIdMap?.get(item.teamId) ?? AFL_API_TEAM_IDS.get(item.teamId) ?? item.teamId
|
|
1649
|
+
),
|
|
1650
|
+
competition: ctx.competition,
|
|
1651
|
+
date: ctx.date ?? null,
|
|
1652
|
+
homeTeam: ctx.homeTeam ?? null,
|
|
1653
|
+
awayTeam: ctx.awayTeam ?? null,
|
|
1654
|
+
playerId: inner.playerId,
|
|
1655
|
+
givenName: inner.playerName.givenName,
|
|
1656
|
+
surname: inner.playerName.surname,
|
|
1657
|
+
displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
|
|
1658
|
+
jumperNumber: item.player.jumperNumber ?? null,
|
|
1659
|
+
kicks: toNullable(stats?.kicks),
|
|
1660
|
+
handballs: toNullable(stats?.handballs),
|
|
1661
|
+
disposals: toNullable(stats?.disposals),
|
|
1662
|
+
marks: toNullable(stats?.marks),
|
|
1663
|
+
goals: toNullable(stats?.goals),
|
|
1664
|
+
behinds: toNullable(stats?.behinds),
|
|
1665
|
+
tackles: toNullable(stats?.tackles),
|
|
1666
|
+
hitouts: toNullable(stats?.hitouts),
|
|
1667
|
+
freesFor: toNullable(stats?.freesFor),
|
|
1668
|
+
freesAgainst: toNullable(stats?.freesAgainst),
|
|
1669
|
+
contestedPossessions: toNullable(stats?.contestedPossessions),
|
|
1670
|
+
uncontestedPossessions: toNullable(stats?.uncontestedPossessions),
|
|
1671
|
+
contestedMarks: toNullable(stats?.contestedMarks),
|
|
1672
|
+
intercepts: toNullable(stats?.intercepts),
|
|
1673
|
+
centreClearances: toNullable(clearances?.centreClearances),
|
|
1674
|
+
stoppageClearances: toNullable(clearances?.stoppageClearances),
|
|
1675
|
+
totalClearances: toNullable(clearances?.totalClearances),
|
|
1676
|
+
inside50s: toNullable(stats?.inside50s),
|
|
1677
|
+
rebound50s: toNullable(stats?.rebound50s),
|
|
1678
|
+
clangers: toNullable(stats?.clangers),
|
|
1679
|
+
turnovers: toNullable(stats?.turnovers),
|
|
1680
|
+
onePercenters: toNullable(stats?.onePercenters),
|
|
1681
|
+
bounces: toNullable(stats?.bounces),
|
|
1682
|
+
goalAssists: toNullable(stats?.goalAssists),
|
|
1683
|
+
disposalEfficiency: toNullable(stats?.disposalEfficiency),
|
|
1684
|
+
metresGained: toNullable(stats?.metresGained),
|
|
1685
|
+
goalAccuracy: toNullable(stats?.goalAccuracy),
|
|
1686
|
+
marksInside50: toNullable(stats?.marksInside50),
|
|
1687
|
+
tacklesInside50: toNullable(stats?.tacklesInside50),
|
|
1688
|
+
shotsAtGoal: toNullable(stats?.shotsAtGoal),
|
|
1689
|
+
scoreInvolvements: toNullable(stats?.scoreInvolvements),
|
|
1690
|
+
totalPossessions: toNullable(stats?.totalPossessions),
|
|
1691
|
+
timeOnGroundPercentage: toNullable(item.playerStats?.timeOnGroundPercentage),
|
|
1692
|
+
ratingPoints: toNullable(stats?.ratingPoints),
|
|
1693
|
+
position: item.player.player.position ?? null,
|
|
1694
|
+
goalEfficiency: toNullable(stats?.goalEfficiency),
|
|
1695
|
+
shotEfficiency: toNullable(stats?.shotEfficiency),
|
|
1696
|
+
interchangeCounts: toNullable(stats?.interchangeCounts),
|
|
1697
|
+
brownlowVotes: toNullable(stats?.brownlowVotes),
|
|
1698
|
+
supercoachScore: null,
|
|
1699
|
+
dreamTeamPoints: toNullable(stats?.dreamTeamPoints),
|
|
1700
|
+
effectiveDisposals: toNullable(stats?.extendedStats?.effectiveDisposals),
|
|
1701
|
+
effectiveKicks: toNullable(stats?.extendedStats?.effectiveKicks),
|
|
1702
|
+
kickEfficiency: toNullable(stats?.extendedStats?.kickEfficiency),
|
|
1703
|
+
kickToHandballRatio: toNullable(stats?.extendedStats?.kickToHandballRatio),
|
|
1704
|
+
pressureActs: toNullable(stats?.extendedStats?.pressureActs),
|
|
1705
|
+
defHalfPressureActs: toNullable(stats?.extendedStats?.defHalfPressureActs),
|
|
1706
|
+
spoils: toNullable(stats?.extendedStats?.spoils),
|
|
1707
|
+
hitoutsToAdvantage: toNullable(stats?.extendedStats?.hitoutsToAdvantage),
|
|
1708
|
+
hitoutWinPercentage: toNullable(stats?.extendedStats?.hitoutWinPercentage),
|
|
1709
|
+
hitoutToAdvantageRate: toNullable(stats?.extendedStats?.hitoutToAdvantageRate),
|
|
1710
|
+
groundBallGets: toNullable(stats?.extendedStats?.groundBallGets),
|
|
1711
|
+
f50GroundBallGets: toNullable(stats?.extendedStats?.f50GroundBallGets),
|
|
1712
|
+
interceptMarks: toNullable(stats?.extendedStats?.interceptMarks),
|
|
1713
|
+
marksOnLead: toNullable(stats?.extendedStats?.marksOnLead),
|
|
1714
|
+
contestedPossessionRate: toNullable(stats?.extendedStats?.contestedPossessionRate),
|
|
1715
|
+
contestOffOneOnOnes: toNullable(stats?.extendedStats?.contestOffOneOnOnes),
|
|
1716
|
+
contestOffWins: toNullable(stats?.extendedStats?.contestOffWins),
|
|
1717
|
+
contestOffWinsPercentage: toNullable(stats?.extendedStats?.contestOffWinsPercentage),
|
|
1718
|
+
contestDefOneOnOnes: toNullable(stats?.extendedStats?.contestDefOneOnOnes),
|
|
1719
|
+
contestDefLosses: toNullable(stats?.extendedStats?.contestDefLosses),
|
|
1720
|
+
contestDefLossPercentage: toNullable(stats?.extendedStats?.contestDefLossPercentage),
|
|
1721
|
+
centreBounceAttendances: toNullable(stats?.extendedStats?.centreBounceAttendances),
|
|
1722
|
+
kickins: toNullable(stats?.extendedStats?.kickins),
|
|
1723
|
+
kickinsPlayon: toNullable(stats?.extendedStats?.kickinsPlayon),
|
|
1724
|
+
ruckContests: toNullable(stats?.extendedStats?.ruckContests),
|
|
1725
|
+
scoreLaunches: toNullable(stats?.extendedStats?.scoreLaunches),
|
|
1726
|
+
source: ctx.source
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function transformPlayerStats(data, ctx) {
|
|
1730
|
+
const home = (data.homeTeamPlayerStats ?? []).map((item) => transformOne(item, ctx));
|
|
1731
|
+
const away = (data.awayTeamPlayerStats ?? []).map((item) => transformOne(item, ctx));
|
|
1732
|
+
return [...home, ...away];
|
|
1586
1733
|
}
|
|
1587
1734
|
|
|
1588
1735
|
// src/lib/validation.ts
|
|
@@ -1885,6 +2032,18 @@ var USER_AGENT = "fitzroy/2 (https://github.com/jackemcpherson/fitzRoy-ts)";
|
|
|
1885
2032
|
var TOKEN_URL = "https://api.afl.com.au/cfs/afl/WMCTok";
|
|
1886
2033
|
var API_BASE = "https://aflapi.afl.com.au/afl/v2";
|
|
1887
2034
|
var CFS_BASE = "https://api.afl.com.au/cfs/afl";
|
|
2035
|
+
var AFL_API_COMP_IDS = {
|
|
2036
|
+
AFLM: 1,
|
|
2037
|
+
AFLW: 3,
|
|
2038
|
+
VFL: 7,
|
|
2039
|
+
VFLW: 11
|
|
2040
|
+
};
|
|
2041
|
+
var AFL_API_TEAM_TYPES = {
|
|
2042
|
+
AFLM: "MEN",
|
|
2043
|
+
AFLW: "WOMEN",
|
|
2044
|
+
VFL: "VFL_MEN",
|
|
2045
|
+
VFLW: "VFL_WOMEN"
|
|
2046
|
+
};
|
|
1888
2047
|
var AflApiClient = class {
|
|
1889
2048
|
fetchFn;
|
|
1890
2049
|
tokenUrl;
|
|
@@ -2059,23 +2218,16 @@ var AflApiClient = class {
|
|
|
2059
2218
|
/**
|
|
2060
2219
|
* Resolve a competition code (e.g. "AFLM") to its API competition ID.
|
|
2061
2220
|
*
|
|
2221
|
+
* Returns the hardcoded mapping from {@link AFL_API_COMP_IDS}. The previous
|
|
2222
|
+
* implementation looked up by `code` field on `/competitions`, but four
|
|
2223
|
+
* competitions share `code="AFL"` (Premiership, Preseason, Origin,
|
|
2224
|
+
* Indigenous All Stars), so the lookup was load-bearing on response order.
|
|
2225
|
+
*
|
|
2062
2226
|
* @param code - The competition code to resolve.
|
|
2063
|
-
* @returns The competition ID
|
|
2227
|
+
* @returns The competition ID on success.
|
|
2064
2228
|
*/
|
|
2065
2229
|
async resolveCompetitionId(code) {
|
|
2066
|
-
|
|
2067
|
-
`${API_BASE}/competitions?pageSize=50`,
|
|
2068
|
-
CompetitionListSchema
|
|
2069
|
-
);
|
|
2070
|
-
if (!result.success) {
|
|
2071
|
-
return result;
|
|
2072
|
-
}
|
|
2073
|
-
const apiCode = code === "AFLM" ? "AFL" : code;
|
|
2074
|
-
const competition = result.data.competitions.find((c) => c.code === apiCode);
|
|
2075
|
-
if (!competition) {
|
|
2076
|
-
return err(new AflApiError(`Competition not found for code: ${code}`));
|
|
2077
|
-
}
|
|
2078
|
-
return ok(competition.id);
|
|
2230
|
+
return ok(AFL_API_COMP_IDS[code]);
|
|
2079
2231
|
}
|
|
2080
2232
|
/**
|
|
2081
2233
|
* Resolve a season (compseason) ID from a competition ID and year.
|
|
@@ -2167,22 +2319,23 @@ var AflApiClient = class {
|
|
|
2167
2319
|
* @param seasonId - The compseason ID.
|
|
2168
2320
|
* @returns Aggregated array of match items from all completed rounds.
|
|
2169
2321
|
*/
|
|
2170
|
-
async fetchSeasonMatchItems(seasonId) {
|
|
2322
|
+
async fetchSeasonMatchItems(seasonId, options) {
|
|
2171
2323
|
const roundsResult = await this.resolveRounds(seasonId);
|
|
2172
2324
|
if (!roundsResult.success) {
|
|
2173
2325
|
return roundsResult;
|
|
2174
2326
|
}
|
|
2175
2327
|
const providerIds = roundsResult.data.flatMap((r) => r.providerId ? [r.providerId] : []);
|
|
2176
2328
|
const results = await batchedMap(providerIds, (id) => this.fetchRoundMatchItems(id));
|
|
2329
|
+
const includeUpcoming = options?.includeUpcoming ?? false;
|
|
2177
2330
|
const allItems = [];
|
|
2178
2331
|
for (const result of results) {
|
|
2179
2332
|
if (!result.success) {
|
|
2180
2333
|
return result;
|
|
2181
2334
|
}
|
|
2182
|
-
const
|
|
2335
|
+
const items = includeUpcoming ? result.data : result.data.filter(
|
|
2183
2336
|
(item) => item.match.status === "CONCLUDED" || item.match.status === "COMPLETE"
|
|
2184
2337
|
);
|
|
2185
|
-
allItems.push(...
|
|
2338
|
+
allItems.push(...items);
|
|
2186
2339
|
}
|
|
2187
2340
|
return ok(allItems);
|
|
2188
2341
|
}
|
|
@@ -2208,17 +2361,21 @@ var AflApiClient = class {
|
|
|
2208
2361
|
return this.fetchJson(`${CFS_BASE}/matchRoster/full/${matchProviderId}`, MatchRosterSchema);
|
|
2209
2362
|
}
|
|
2210
2363
|
/**
|
|
2211
|
-
* Fetch team list, optionally filtered by
|
|
2364
|
+
* Fetch team list, optionally filtered by competition.
|
|
2365
|
+
*
|
|
2366
|
+
* Pass a `CompetitionCode` to scope the result to that competition's teams
|
|
2367
|
+
* (uses {@link AFL_API_TEAM_TYPES} internally).
|
|
2212
2368
|
*
|
|
2213
|
-
* @param
|
|
2369
|
+
* @param competition - Optional CompetitionCode filter (e.g. "AFLM", "VFL").
|
|
2214
2370
|
* @returns Array of team items.
|
|
2215
2371
|
*/
|
|
2216
|
-
async fetchTeams(
|
|
2217
|
-
const result = await this.fetchJson(`${API_BASE}/teams?pageSize=
|
|
2372
|
+
async fetchTeams(competition) {
|
|
2373
|
+
const result = await this.fetchJson(`${API_BASE}/teams?pageSize=500`, TeamListSchema);
|
|
2218
2374
|
if (!result.success) {
|
|
2219
2375
|
return result;
|
|
2220
2376
|
}
|
|
2221
|
-
if (
|
|
2377
|
+
if (competition) {
|
|
2378
|
+
const teamType = AFL_API_TEAM_TYPES[competition];
|
|
2222
2379
|
return ok(result.data.teams.filter((t) => t.teamType === teamType));
|
|
2223
2380
|
}
|
|
2224
2381
|
return ok(result.data.teams);
|
|
@@ -2252,281 +2409,282 @@ var AflApiClient = class {
|
|
|
2252
2409
|
}
|
|
2253
2410
|
};
|
|
2254
2411
|
|
|
2255
|
-
// src/
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
ateamid: z2.number(),
|
|
2266
|
-
hscore: z2.number().nullable(),
|
|
2267
|
-
ascore: z2.number().nullable(),
|
|
2268
|
-
hgoals: z2.number().nullable(),
|
|
2269
|
-
agoals: z2.number().nullable(),
|
|
2270
|
-
hbehinds: z2.number().nullable(),
|
|
2271
|
-
abehinds: z2.number().nullable(),
|
|
2272
|
-
winner: z2.string().nullable(),
|
|
2273
|
-
winnerteamid: z2.number().nullable(),
|
|
2274
|
-
venue: z2.string(),
|
|
2275
|
-
date: z2.string(),
|
|
2276
|
-
localtime: z2.string(),
|
|
2277
|
-
tz: z2.string(),
|
|
2278
|
-
unixtime: z2.number(),
|
|
2279
|
-
timestr: z2.string().nullable(),
|
|
2280
|
-
complete: z2.number(),
|
|
2281
|
-
is_final: z2.number(),
|
|
2282
|
-
is_grand_final: z2.number(),
|
|
2283
|
-
updated: z2.string()
|
|
2284
|
-
});
|
|
2285
|
-
var SquiggleGamesResponseSchema = z2.object({
|
|
2286
|
-
games: z2.array(SquiggleGameSchema)
|
|
2287
|
-
});
|
|
2288
|
-
var SquiggleStandingSchema = z2.object({
|
|
2289
|
-
id: z2.number(),
|
|
2290
|
-
name: z2.string(),
|
|
2291
|
-
rank: z2.number(),
|
|
2292
|
-
played: z2.number(),
|
|
2293
|
-
wins: z2.number(),
|
|
2294
|
-
losses: z2.number(),
|
|
2295
|
-
draws: z2.number(),
|
|
2296
|
-
pts: z2.number(),
|
|
2297
|
-
for: z2.number(),
|
|
2298
|
-
against: z2.number(),
|
|
2299
|
-
percentage: z2.number(),
|
|
2300
|
-
goals_for: z2.number(),
|
|
2301
|
-
goals_against: z2.number(),
|
|
2302
|
-
behinds_for: z2.number(),
|
|
2303
|
-
behinds_against: z2.number()
|
|
2304
|
-
});
|
|
2305
|
-
var SquiggleStandingsResponseSchema = z2.object({
|
|
2306
|
-
standings: z2.array(SquiggleStandingSchema)
|
|
2307
|
-
});
|
|
2308
|
-
|
|
2309
|
-
// src/sources/squiggle.ts
|
|
2310
|
-
var SQUIGGLE_BASE = "https://api.squiggle.com.au/";
|
|
2311
|
-
var USER_AGENT2 = "fitzRoy-ts/1.0 (https://github.com/jackemcpherson/fitzRoy-ts)";
|
|
2312
|
-
var SquiggleClient = class {
|
|
2313
|
-
fetchFn;
|
|
2314
|
-
constructor(options) {
|
|
2315
|
-
this.fetchFn = options?.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
2412
|
+
// src/sources/adapters/afl-api.ts
|
|
2413
|
+
var AFL_API_COVERAGE = /* @__PURE__ */ new Map([
|
|
2414
|
+
["AFLM", { minSeason: 2012 }],
|
|
2415
|
+
["AFLW", { minSeason: 2017 }],
|
|
2416
|
+
["VFL", { minSeason: 2021 }],
|
|
2417
|
+
["VFLW", { minSeason: 2021 }]
|
|
2418
|
+
]);
|
|
2419
|
+
var AflApiMatchSource = class {
|
|
2420
|
+
constructor(client = new AflApiClient()) {
|
|
2421
|
+
this.client = client;
|
|
2316
2422
|
}
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2423
|
+
id = "afl-api";
|
|
2424
|
+
coverage = AFL_API_COVERAGE;
|
|
2425
|
+
async fetchMatches(query) {
|
|
2426
|
+
const competition = query.competition ?? "AFLM";
|
|
2427
|
+
const seasonResult = await this.client.resolveCompSeason(competition, query.season);
|
|
2428
|
+
if (!seasonResult.success) return seasonResult;
|
|
2429
|
+
const includeUpcoming = query.status !== "Complete";
|
|
2430
|
+
const itemsResult = query.round != null ? await this.client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round) : await this.client.fetchSeasonMatchItems(seasonResult.data, { includeUpcoming });
|
|
2431
|
+
if (!itemsResult.success) return itemsResult;
|
|
2432
|
+
return ok(transformMatchItems(itemsResult.data, query.season, competition));
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
var AflApiPlayerStatsSource = class {
|
|
2436
|
+
constructor(client = new AflApiClient()) {
|
|
2437
|
+
this.client = client;
|
|
2438
|
+
}
|
|
2439
|
+
id = "afl-api";
|
|
2440
|
+
coverage = AFL_API_COVERAGE;
|
|
2441
|
+
async fetchPlayerStats(query) {
|
|
2442
|
+
const competition = query.competition ?? "AFLM";
|
|
2443
|
+
if (query.matchId) {
|
|
2444
|
+
const [rosterResult, statsResult] = await Promise.all([
|
|
2445
|
+
this.client.fetchMatchRoster(query.matchId),
|
|
2446
|
+
this.client.fetchPlayerStats(query.matchId)
|
|
2447
|
+
]);
|
|
2448
|
+
if (!statsResult.success) return statsResult;
|
|
2449
|
+
const teamIdMap2 = new Map(AFL_API_TEAM_IDS);
|
|
2450
|
+
if (rosterResult.success) {
|
|
2451
|
+
const match = rosterResult.data.match;
|
|
2452
|
+
teamIdMap2.set(match.homeTeamId, normaliseTeamName(match.homeTeam.name));
|
|
2453
|
+
teamIdMap2.set(match.awayTeamId, normaliseTeamName(match.awayTeam.name));
|
|
2330
2454
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
"
|
|
2338
|
-
|
|
2455
|
+
return ok(
|
|
2456
|
+
transformPlayerStats(statsResult.data, {
|
|
2457
|
+
matchId: query.matchId,
|
|
2458
|
+
season: query.season,
|
|
2459
|
+
roundNumber: query.round ?? 0,
|
|
2460
|
+
competition,
|
|
2461
|
+
source: "afl-api",
|
|
2462
|
+
teamIdMap: teamIdMap2
|
|
2463
|
+
})
|
|
2339
2464
|
);
|
|
2340
2465
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2466
|
+
const seasonResult = await this.client.resolveCompSeason(competition, query.season);
|
|
2467
|
+
if (!seasonResult.success) return seasonResult;
|
|
2468
|
+
const matchItemsResult = query.round != null ? await this.client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round) : await this.client.fetchSeasonMatchItems(seasonResult.data);
|
|
2469
|
+
if (!matchItemsResult.success) return matchItemsResult;
|
|
2470
|
+
const teamIdMap = /* @__PURE__ */ new Map();
|
|
2471
|
+
for (const item of matchItemsResult.data) {
|
|
2472
|
+
teamIdMap.set(item.match.homeTeamId, item.match.homeTeam.name);
|
|
2473
|
+
teamIdMap.set(item.match.awayTeamId, item.match.awayTeam.name);
|
|
2474
|
+
}
|
|
2475
|
+
const statsResults = await batchedMap(
|
|
2476
|
+
matchItemsResult.data,
|
|
2477
|
+
(item) => this.client.fetchPlayerStats(item.match.matchId)
|
|
2478
|
+
);
|
|
2479
|
+
const allStats = [];
|
|
2480
|
+
for (let i = 0; i < statsResults.length; i++) {
|
|
2481
|
+
const statsResult = statsResults[i];
|
|
2482
|
+
if (!statsResult?.success) {
|
|
2483
|
+
return statsResult ?? err(new AflApiError("Missing stats result"));
|
|
2484
|
+
}
|
|
2485
|
+
const item = matchItemsResult.data[i];
|
|
2486
|
+
if (!item) continue;
|
|
2487
|
+
allStats.push(
|
|
2488
|
+
...transformPlayerStats(statsResult.data, {
|
|
2489
|
+
matchId: item.match.matchId,
|
|
2490
|
+
season: query.season,
|
|
2491
|
+
roundNumber: item.round?.roundNumber ?? query.round ?? 0,
|
|
2492
|
+
competition,
|
|
2493
|
+
source: "afl-api",
|
|
2494
|
+
teamIdMap,
|
|
2495
|
+
date: parseDate(item.match.utcStartTime) ?? new Date(item.match.utcStartTime),
|
|
2496
|
+
homeTeam: normaliseTeamName(item.match.homeTeam.name),
|
|
2497
|
+
awayTeam: normaliseTeamName(item.match.awayTeam.name)
|
|
2498
|
+
})
|
|
2359
2499
|
);
|
|
2360
2500
|
}
|
|
2361
|
-
return ok(
|
|
2501
|
+
return ok(allStats);
|
|
2362
2502
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
const
|
|
2373
|
-
if (!
|
|
2374
|
-
const
|
|
2375
|
-
if (!
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2503
|
+
};
|
|
2504
|
+
var AflApiSquadSource = class {
|
|
2505
|
+
constructor(client = new AflApiClient()) {
|
|
2506
|
+
this.client = client;
|
|
2507
|
+
}
|
|
2508
|
+
id = "afl-api";
|
|
2509
|
+
coverage = AFL_API_COVERAGE;
|
|
2510
|
+
async fetchSquad(query) {
|
|
2511
|
+
const competition = query.competition ?? "AFLM";
|
|
2512
|
+
const seasonResult = await this.client.resolveCompSeason(competition, query.season);
|
|
2513
|
+
if (!seasonResult.success) return seasonResult;
|
|
2514
|
+
const teamIdResult = await this.resolveTeamId(query.team, competition);
|
|
2515
|
+
if (!teamIdResult.success) return teamIdResult;
|
|
2516
|
+
const squadResult = await this.client.fetchSquad(teamIdResult.data, seasonResult.data);
|
|
2517
|
+
if (!squadResult.success) return squadResult;
|
|
2518
|
+
const players = squadResult.data.squad.players.map((p) => ({
|
|
2519
|
+
playerId: p.player.providerId ?? String(p.player.id),
|
|
2520
|
+
givenName: p.player.firstName,
|
|
2521
|
+
surname: p.player.surname,
|
|
2522
|
+
displayName: `${p.player.firstName} ${p.player.surname}`,
|
|
2523
|
+
jumperNumber: p.jumperNumber ?? null,
|
|
2524
|
+
position: p.position ?? null,
|
|
2525
|
+
dateOfBirth: p.player.dateOfBirth ? parseDate(p.player.dateOfBirth) : null,
|
|
2526
|
+
heightCm: p.player.heightInCm || null,
|
|
2527
|
+
weightKg: p.player.weightInKg || null,
|
|
2528
|
+
draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
|
|
2529
|
+
draftPosition: p.player.draftPosition ? Number.parseInt(p.player.draftPosition, 10) || null : null,
|
|
2530
|
+
draftType: p.player.draftType ?? null,
|
|
2531
|
+
debutYear: p.player.debutYear ? Number.parseInt(p.player.debutYear, 10) || null : null,
|
|
2532
|
+
recruitedFrom: p.player.recruitedFrom ?? null
|
|
2533
|
+
}));
|
|
2534
|
+
return ok({
|
|
2535
|
+
teamId: String(teamIdResult.data),
|
|
2536
|
+
teamName: normaliseTeamName(squadResult.data.squad.team?.name ?? query.team),
|
|
2537
|
+
season: query.season,
|
|
2538
|
+
players,
|
|
2539
|
+
competition
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
/** Resolve a canonical team name to the AFL API's numeric team ID. */
|
|
2543
|
+
async resolveTeamId(teamName, competition) {
|
|
2544
|
+
const teamsResult = await this.client.fetchTeams(competition);
|
|
2545
|
+
if (!teamsResult.success) return teamsResult;
|
|
2546
|
+
const normalised = normaliseTeamName(teamName);
|
|
2547
|
+
const match = teamsResult.data.find((t) => normaliseTeamName(t.name) === normalised);
|
|
2548
|
+
if (!match) {
|
|
2549
|
+
return err(new ValidationError(`Team not found in ${competition}: ${teamName}`));
|
|
2379
2550
|
}
|
|
2380
|
-
return ok(
|
|
2551
|
+
return ok(match.id);
|
|
2381
2552
|
}
|
|
2382
2553
|
};
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
if (complete === 100) return "Complete";
|
|
2387
|
-
if (complete > 0) return "Live";
|
|
2388
|
-
return "Upcoming";
|
|
2389
|
-
}
|
|
2390
|
-
function transformSquiggleGamesToResults(games, season) {
|
|
2391
|
-
return games.filter((g) => g.complete === 100).map((g) => ({
|
|
2392
|
-
matchId: `SQ_${g.id}`,
|
|
2393
|
-
season,
|
|
2394
|
-
roundNumber: g.round,
|
|
2395
|
-
roundType: inferRoundType(g.roundname),
|
|
2396
|
-
roundName: g.roundname || null,
|
|
2397
|
-
date: parseDate(g.unixtime) ?? new Date(g.unixtime * 1e3),
|
|
2398
|
-
venue: normaliseVenueName(g.venue),
|
|
2399
|
-
homeTeam: normaliseTeamName(g.hteam),
|
|
2400
|
-
awayTeam: normaliseTeamName(g.ateam),
|
|
2401
|
-
homeGoals: g.hgoals ?? 0,
|
|
2402
|
-
homeBehinds: g.hbehinds ?? 0,
|
|
2403
|
-
homePoints: g.hscore ?? 0,
|
|
2404
|
-
awayGoals: g.agoals ?? 0,
|
|
2405
|
-
awayBehinds: g.abehinds ?? 0,
|
|
2406
|
-
awayPoints: g.ascore ?? 0,
|
|
2407
|
-
margin: (g.hscore ?? 0) - (g.ascore ?? 0),
|
|
2408
|
-
q1Home: null,
|
|
2409
|
-
q2Home: null,
|
|
2410
|
-
q3Home: null,
|
|
2411
|
-
q4Home: null,
|
|
2412
|
-
q1Away: null,
|
|
2413
|
-
q2Away: null,
|
|
2414
|
-
q3Away: null,
|
|
2415
|
-
q4Away: null,
|
|
2416
|
-
status: "Complete",
|
|
2417
|
-
attendance: null,
|
|
2418
|
-
weatherTempCelsius: null,
|
|
2419
|
-
weatherType: null,
|
|
2420
|
-
roundCode: toRoundCode(g.roundname),
|
|
2421
|
-
venueState: null,
|
|
2422
|
-
venueTimezone: g.tz || null,
|
|
2423
|
-
homeRushedBehinds: null,
|
|
2424
|
-
awayRushedBehinds: null,
|
|
2425
|
-
homeMinutesInFront: null,
|
|
2426
|
-
awayMinutesInFront: null,
|
|
2427
|
-
source: "squiggle",
|
|
2428
|
-
competition: "AFLM"
|
|
2429
|
-
}));
|
|
2430
|
-
}
|
|
2431
|
-
function transformSquiggleGamesToFixture(games, season) {
|
|
2432
|
-
return games.map((g) => ({
|
|
2433
|
-
matchId: `SQ_${g.id}`,
|
|
2434
|
-
season,
|
|
2435
|
-
roundNumber: g.round,
|
|
2436
|
-
roundType: inferRoundType(g.roundname),
|
|
2437
|
-
date: parseDate(g.unixtime) ?? new Date(g.unixtime * 1e3),
|
|
2438
|
-
venue: normaliseVenueName(g.venue),
|
|
2439
|
-
homeTeam: normaliseTeamName(g.hteam),
|
|
2440
|
-
awayTeam: normaliseTeamName(g.ateam),
|
|
2441
|
-
status: toMatchStatus2(g.complete),
|
|
2442
|
-
competition: "AFLM"
|
|
2443
|
-
}));
|
|
2444
|
-
}
|
|
2445
|
-
function transformSquiggleStandings(standings) {
|
|
2446
|
-
return standings.map((s) => ({
|
|
2447
|
-
position: s.rank,
|
|
2448
|
-
team: normaliseTeamName(s.name),
|
|
2449
|
-
played: s.played,
|
|
2450
|
-
wins: s.wins,
|
|
2451
|
-
losses: s.losses,
|
|
2452
|
-
draws: s.draws,
|
|
2453
|
-
pointsFor: s.for,
|
|
2454
|
-
pointsAgainst: s.against,
|
|
2455
|
-
percentage: s.percentage,
|
|
2456
|
-
premiershipsPoints: s.pts,
|
|
2457
|
-
form: null
|
|
2458
|
-
}));
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
// src/api/fixture.ts
|
|
2462
|
-
function toFixture(item, season, fallbackRoundNumber, competition) {
|
|
2463
|
-
return {
|
|
2464
|
-
matchId: item.match.matchId,
|
|
2465
|
-
season,
|
|
2466
|
-
roundNumber: item.round?.roundNumber ?? fallbackRoundNumber,
|
|
2467
|
-
roundType: inferRoundType(item.round?.name ?? ""),
|
|
2468
|
-
date: parseDate(item.match.utcStartTime) ?? new Date(item.match.utcStartTime),
|
|
2469
|
-
venue: normaliseVenueName(item.venue?.name ?? ""),
|
|
2470
|
-
homeTeam: normaliseTeamName(item.match.homeTeam.name),
|
|
2471
|
-
awayTeam: normaliseTeamName(item.match.awayTeam.name),
|
|
2472
|
-
status: toMatchStatus(item.match.status),
|
|
2473
|
-
competition
|
|
2474
|
-
};
|
|
2475
|
-
}
|
|
2476
|
-
async function fetchFixture(query) {
|
|
2477
|
-
const competition = query.competition ?? "AFLM";
|
|
2478
|
-
if (query.source === "squiggle") {
|
|
2479
|
-
if (competition === "AFLW") return err(aflwUnsupportedError("squiggle"));
|
|
2480
|
-
const client2 = new SquiggleClient();
|
|
2481
|
-
const result = await client2.fetchGames(query.season, query.round ?? void 0);
|
|
2482
|
-
if (!result.success) return result;
|
|
2483
|
-
return ok(transformSquiggleGamesToFixture(result.data.games, query.season));
|
|
2554
|
+
var AflApiLineupSource = class {
|
|
2555
|
+
constructor(client = new AflApiClient()) {
|
|
2556
|
+
this.client = client;
|
|
2484
2557
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
const
|
|
2489
|
-
if (
|
|
2490
|
-
|
|
2491
|
-
|
|
2558
|
+
id = "afl-api";
|
|
2559
|
+
coverage = AFL_API_COVERAGE;
|
|
2560
|
+
async fetchLineup(query) {
|
|
2561
|
+
const competition = query.competition ?? "AFLM";
|
|
2562
|
+
if (query.matchId) {
|
|
2563
|
+
const rosterResult = await this.client.fetchMatchRoster(query.matchId);
|
|
2564
|
+
if (!rosterResult.success) return rosterResult;
|
|
2565
|
+
return ok([transformMatchRoster(rosterResult.data, query.season, query.round, competition)]);
|
|
2492
2566
|
}
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2567
|
+
const seasonResult = await this.client.resolveCompSeason(competition, query.season);
|
|
2568
|
+
if (!seasonResult.success) return seasonResult;
|
|
2569
|
+
const matchItems = await this.client.fetchRoundMatchItemsByNumber(
|
|
2570
|
+
seasonResult.data,
|
|
2571
|
+
query.round
|
|
2572
|
+
);
|
|
2573
|
+
if (!matchItems.success) return matchItems;
|
|
2574
|
+
if (matchItems.data.length === 0) {
|
|
2575
|
+
return err(new AflApiError(`No matches found for round ${query.round}`));
|
|
2576
|
+
}
|
|
2577
|
+
const rosterResults = await batchedMap(
|
|
2578
|
+
matchItems.data,
|
|
2579
|
+
(item) => this.client.fetchMatchRoster(item.match.matchId)
|
|
2501
2580
|
);
|
|
2581
|
+
const lineups = [];
|
|
2582
|
+
for (const rosterResult of rosterResults) {
|
|
2583
|
+
if (!rosterResult.success) return rosterResult;
|
|
2584
|
+
lineups.push(transformMatchRoster(rosterResult.data, query.season, query.round, competition));
|
|
2585
|
+
}
|
|
2586
|
+
return ok(lineups);
|
|
2502
2587
|
}
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
const itemsResult = await client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round);
|
|
2508
|
-
if (!itemsResult.success) return itemsResult;
|
|
2509
|
-
return ok(itemsResult.data.map((item) => toFixture(item, query.season, 0, competition)));
|
|
2588
|
+
};
|
|
2589
|
+
var AflApiLadderSource = class {
|
|
2590
|
+
constructor(client = new AflApiClient()) {
|
|
2591
|
+
this.client = client;
|
|
2510
2592
|
}
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
(
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2593
|
+
id = "afl-api";
|
|
2594
|
+
coverage = AFL_API_COVERAGE;
|
|
2595
|
+
async fetchLadder(query) {
|
|
2596
|
+
const competition = query.competition ?? "AFLM";
|
|
2597
|
+
const seasonResult = await this.client.resolveCompSeason(competition, query.season);
|
|
2598
|
+
if (!seasonResult.success) return seasonResult;
|
|
2599
|
+
let roundId;
|
|
2600
|
+
if (query.round != null) {
|
|
2601
|
+
const roundsResult = await this.client.resolveRounds(seasonResult.data);
|
|
2602
|
+
if (!roundsResult.success) return roundsResult;
|
|
2603
|
+
const round = roundsResult.data.find((r) => r.roundNumber === query.round);
|
|
2604
|
+
if (round) {
|
|
2605
|
+
roundId = round.id;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
const ladderResult = await this.client.fetchLadder(seasonResult.data, roundId);
|
|
2609
|
+
if (!ladderResult.success) return ladderResult;
|
|
2610
|
+
const firstLadder = ladderResult.data.ladders[0];
|
|
2611
|
+
const entries = firstLadder ? transformLadderEntries(firstLadder.entries) : [];
|
|
2612
|
+
return ok({
|
|
2613
|
+
season: query.season,
|
|
2614
|
+
roundNumber: ladderResult.data.round?.roundNumber ?? null,
|
|
2615
|
+
entries,
|
|
2616
|
+
competition
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
|
|
2621
|
+
// src/transforms/computed-ladder.ts
|
|
2622
|
+
function computeLadder(results, upToRound) {
|
|
2623
|
+
const teams = /* @__PURE__ */ new Map();
|
|
2624
|
+
const filtered = upToRound != null ? results.filter((r) => r.roundType === "HomeAndAway" && r.roundNumber <= upToRound) : results.filter((r) => r.roundType === "HomeAndAway");
|
|
2625
|
+
for (const match of filtered) {
|
|
2626
|
+
if (match.status !== "Complete") continue;
|
|
2627
|
+
if (match.homePoints === null || match.awayPoints === null) continue;
|
|
2628
|
+
const homePoints = match.homePoints;
|
|
2629
|
+
const awayPoints = match.awayPoints;
|
|
2630
|
+
const home = getOrCreate(teams, match.homeTeam);
|
|
2631
|
+
const away = getOrCreate(teams, match.awayTeam);
|
|
2632
|
+
home.played++;
|
|
2633
|
+
away.played++;
|
|
2634
|
+
home.pointsFor += homePoints;
|
|
2635
|
+
home.pointsAgainst += awayPoints;
|
|
2636
|
+
away.pointsFor += awayPoints;
|
|
2637
|
+
away.pointsAgainst += homePoints;
|
|
2638
|
+
if (homePoints > awayPoints) {
|
|
2639
|
+
home.wins++;
|
|
2640
|
+
away.losses++;
|
|
2641
|
+
} else if (awayPoints > homePoints) {
|
|
2642
|
+
away.wins++;
|
|
2643
|
+
home.losses++;
|
|
2644
|
+
} else {
|
|
2645
|
+
home.draws++;
|
|
2646
|
+
away.draws++;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
const entries = [...teams.entries()].map(([teamName, acc]) => {
|
|
2650
|
+
const percentage = acc.pointsAgainst === 0 ? 0 : acc.pointsFor / acc.pointsAgainst * 100;
|
|
2651
|
+
const premiershipsPoints = acc.wins * 4 + acc.draws * 2;
|
|
2652
|
+
return {
|
|
2653
|
+
position: 0,
|
|
2654
|
+
// filled below after sorting
|
|
2655
|
+
team: teamName,
|
|
2656
|
+
played: acc.played,
|
|
2657
|
+
wins: acc.wins,
|
|
2658
|
+
losses: acc.losses,
|
|
2659
|
+
draws: acc.draws,
|
|
2660
|
+
pointsFor: acc.pointsFor,
|
|
2661
|
+
pointsAgainst: acc.pointsAgainst,
|
|
2662
|
+
percentage,
|
|
2663
|
+
premiershipsPoints,
|
|
2664
|
+
form: null
|
|
2665
|
+
};
|
|
2666
|
+
});
|
|
2667
|
+
entries.sort((a, b) => {
|
|
2668
|
+
if (b.premiershipsPoints !== a.premiershipsPoints) {
|
|
2669
|
+
return b.premiershipsPoints - a.premiershipsPoints;
|
|
2670
|
+
}
|
|
2671
|
+
return b.percentage - a.percentage;
|
|
2672
|
+
});
|
|
2673
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2674
|
+
const entry = entries[i];
|
|
2675
|
+
if (entry) {
|
|
2676
|
+
entries[i] = { ...entry, position: i + 1 };
|
|
2527
2677
|
}
|
|
2528
2678
|
}
|
|
2529
|
-
return
|
|
2679
|
+
return entries;
|
|
2680
|
+
}
|
|
2681
|
+
function getOrCreate(map, team) {
|
|
2682
|
+
let acc = map.get(team);
|
|
2683
|
+
if (!acc) {
|
|
2684
|
+
acc = { played: 0, wins: 0, losses: 0, draws: 0, pointsFor: 0, pointsAgainst: 0 };
|
|
2685
|
+
map.set(team, acc);
|
|
2686
|
+
}
|
|
2687
|
+
return acc;
|
|
2530
2688
|
}
|
|
2531
2689
|
|
|
2532
2690
|
// src/sources/afl-tables.ts
|
|
@@ -3076,1043 +3234,1209 @@ function parseAflTablesPlayerList(html, teamName) {
|
|
|
3076
3234
|
goals: goalsScored,
|
|
3077
3235
|
draftYear: null,
|
|
3078
3236
|
draftPosition: null,
|
|
3079
|
-
draftType: null,
|
|
3080
|
-
debutYear,
|
|
3081
|
-
recruitedFrom: null
|
|
3082
|
-
});
|
|
3083
|
-
});
|
|
3084
|
-
return players;
|
|
3085
|
-
}
|
|
3086
|
-
|
|
3087
|
-
// src/transforms/computed-ladder.ts
|
|
3088
|
-
function computeLadder(results, upToRound) {
|
|
3089
|
-
const teams = /* @__PURE__ */ new Map();
|
|
3090
|
-
const filtered = upToRound != null ? results.filter((r) => r.roundType === "HomeAndAway" && r.roundNumber <= upToRound) : results.filter((r) => r.roundType === "HomeAndAway");
|
|
3091
|
-
for (const match of filtered) {
|
|
3092
|
-
if (match.status !== "Complete") continue;
|
|
3093
|
-
const home = getOrCreate(teams, match.homeTeam);
|
|
3094
|
-
const away = getOrCreate(teams, match.awayTeam);
|
|
3095
|
-
home.played++;
|
|
3096
|
-
away.played++;
|
|
3097
|
-
home.pointsFor += match.homePoints;
|
|
3098
|
-
home.pointsAgainst += match.awayPoints;
|
|
3099
|
-
away.pointsFor += match.awayPoints;
|
|
3100
|
-
away.pointsAgainst += match.homePoints;
|
|
3101
|
-
if (match.homePoints > match.awayPoints) {
|
|
3102
|
-
home.wins++;
|
|
3103
|
-
away.losses++;
|
|
3104
|
-
} else if (match.awayPoints > match.homePoints) {
|
|
3105
|
-
away.wins++;
|
|
3106
|
-
home.losses++;
|
|
3107
|
-
} else {
|
|
3108
|
-
home.draws++;
|
|
3109
|
-
away.draws++;
|
|
3110
|
-
}
|
|
3111
|
-
}
|
|
3112
|
-
const entries = [...teams.entries()].map(([teamName, acc]) => {
|
|
3113
|
-
const percentage = acc.pointsAgainst === 0 ? 0 : acc.pointsFor / acc.pointsAgainst * 100;
|
|
3114
|
-
const premiershipsPoints = acc.wins * 4 + acc.draws * 2;
|
|
3115
|
-
return {
|
|
3116
|
-
position: 0,
|
|
3117
|
-
// filled below after sorting
|
|
3118
|
-
team: teamName,
|
|
3119
|
-
played: acc.played,
|
|
3120
|
-
wins: acc.wins,
|
|
3121
|
-
losses: acc.losses,
|
|
3122
|
-
draws: acc.draws,
|
|
3123
|
-
pointsFor: acc.pointsFor,
|
|
3124
|
-
pointsAgainst: acc.pointsAgainst,
|
|
3125
|
-
percentage,
|
|
3126
|
-
premiershipsPoints,
|
|
3127
|
-
form: null
|
|
3128
|
-
};
|
|
3129
|
-
});
|
|
3130
|
-
entries.sort((a, b) => {
|
|
3131
|
-
if (b.premiershipsPoints !== a.premiershipsPoints) {
|
|
3132
|
-
return b.premiershipsPoints - a.premiershipsPoints;
|
|
3133
|
-
}
|
|
3134
|
-
return b.percentage - a.percentage;
|
|
3135
|
-
});
|
|
3136
|
-
for (let i = 0; i < entries.length; i++) {
|
|
3137
|
-
const entry = entries[i];
|
|
3138
|
-
if (entry) {
|
|
3139
|
-
entries[i] = { ...entry, position: i + 1 };
|
|
3140
|
-
}
|
|
3141
|
-
}
|
|
3142
|
-
return entries;
|
|
3143
|
-
}
|
|
3144
|
-
function getOrCreate(map, team) {
|
|
3145
|
-
let acc = map.get(team);
|
|
3146
|
-
if (!acc) {
|
|
3147
|
-
acc = { played: 0, wins: 0, losses: 0, draws: 0, pointsFor: 0, pointsAgainst: 0 };
|
|
3148
|
-
map.set(team, acc);
|
|
3149
|
-
}
|
|
3150
|
-
return acc;
|
|
3151
|
-
}
|
|
3152
|
-
|
|
3153
|
-
// src/transforms/ladder.ts
|
|
3154
|
-
function transformLadderEntries(entries) {
|
|
3155
|
-
return entries.map((entry) => {
|
|
3156
|
-
const record = entry.thisSeasonRecord;
|
|
3157
|
-
const wl = record?.winLossRecord;
|
|
3158
|
-
return {
|
|
3159
|
-
position: entry.position,
|
|
3160
|
-
team: normaliseTeamName(entry.team.name),
|
|
3161
|
-
played: entry.played ?? wl?.played ?? 0,
|
|
3162
|
-
wins: wl?.wins ?? 0,
|
|
3163
|
-
losses: wl?.losses ?? 0,
|
|
3164
|
-
draws: wl?.draws ?? 0,
|
|
3165
|
-
pointsFor: entry.pointsFor ?? 0,
|
|
3166
|
-
pointsAgainst: entry.pointsAgainst ?? 0,
|
|
3167
|
-
percentage: record?.percentage ?? 0,
|
|
3168
|
-
premiershipsPoints: record?.aggregatePoints ?? 0,
|
|
3169
|
-
form: entry.form ?? null
|
|
3170
|
-
};
|
|
3237
|
+
draftType: null,
|
|
3238
|
+
debutYear,
|
|
3239
|
+
recruitedFrom: null
|
|
3240
|
+
});
|
|
3171
3241
|
});
|
|
3242
|
+
return players;
|
|
3172
3243
|
}
|
|
3173
3244
|
|
|
3174
|
-
// src/
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3245
|
+
// src/sources/adapters/afl-tables.ts
|
|
3246
|
+
var AFL_TABLES_MATCH_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 1897 }]]);
|
|
3247
|
+
var AFL_TABLES_PLAYER_STATS_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 1965 }]]);
|
|
3248
|
+
var AFL_TABLES_TEAM_STATS_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 1965 }]]);
|
|
3249
|
+
var AFL_TABLES_LADDER_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 1897 }]]);
|
|
3250
|
+
var AFL_TABLES_SQUAD_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 1897 }]]);
|
|
3251
|
+
var AflTablesMatchSource = class {
|
|
3252
|
+
constructor(client = new AflTablesClient()) {
|
|
3253
|
+
this.client = client;
|
|
3254
|
+
}
|
|
3255
|
+
id = "afl-tables";
|
|
3256
|
+
coverage = AFL_TABLES_MATCH_COVERAGE;
|
|
3257
|
+
async fetchMatches(query) {
|
|
3258
|
+
const result = await this.client.fetchSeasonResults(query.season);
|
|
3259
|
+
if (!result.success) return result;
|
|
3260
|
+
const filtered = query.round != null ? result.data.filter((m) => m.roundNumber === query.round) : result.data;
|
|
3261
|
+
return ok(filtered);
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
var AflTablesPlayerStatsSource = class {
|
|
3265
|
+
constructor(client = new AflTablesClient()) {
|
|
3266
|
+
this.client = client;
|
|
3267
|
+
}
|
|
3268
|
+
id = "afl-tables";
|
|
3269
|
+
coverage = AFL_TABLES_PLAYER_STATS_COVERAGE;
|
|
3270
|
+
async fetchPlayerStats(query) {
|
|
3271
|
+
const result = await this.client.fetchSeasonPlayerStats(query.season);
|
|
3272
|
+
if (!result.success) return result;
|
|
3273
|
+
if (query.round != null) {
|
|
3274
|
+
return ok(result.data.filter((s) => s.roundNumber === query.round));
|
|
3275
|
+
}
|
|
3276
|
+
return result;
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3279
|
+
var AflTablesTeamStatsSource = class {
|
|
3280
|
+
constructor(client = new AflTablesClient()) {
|
|
3281
|
+
this.client = client;
|
|
3282
|
+
}
|
|
3283
|
+
id = "afl-tables";
|
|
3284
|
+
coverage = AFL_TABLES_TEAM_STATS_COVERAGE;
|
|
3285
|
+
async fetchTeamStats(query) {
|
|
3286
|
+
const summaryType = query.summaryType ?? "totals";
|
|
3287
|
+
const statsResult = await this.client.fetchTeamStats(query.season);
|
|
3288
|
+
if (!statsResult.success) return statsResult;
|
|
3289
|
+
const needsGp = statsResult.data.some((e) => e.gamesPlayed === 0);
|
|
3290
|
+
const gpMap = /* @__PURE__ */ new Map();
|
|
3291
|
+
if (needsGp) {
|
|
3292
|
+
const resultsResult = await this.client.fetchSeasonResults(query.season);
|
|
3293
|
+
if (resultsResult.success) {
|
|
3294
|
+
for (const m of resultsResult.data) {
|
|
3295
|
+
const home = normaliseTeamName(m.homeTeam);
|
|
3296
|
+
const away = normaliseTeamName(m.awayTeam);
|
|
3297
|
+
gpMap.set(home, (gpMap.get(home) ?? 0) + 1);
|
|
3298
|
+
gpMap.set(away, (gpMap.get(away) ?? 0) + 1);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
const enriched = statsResult.data.map((entry) => ({
|
|
3303
|
+
...entry,
|
|
3304
|
+
gamesPlayed: gpMap.get(normaliseTeamName(entry.team)) ?? entry.gamesPlayed
|
|
3305
|
+
}));
|
|
3306
|
+
if (summaryType === "averages") {
|
|
3307
|
+
return ok(
|
|
3308
|
+
enriched.map((entry) => ({
|
|
3309
|
+
...entry,
|
|
3310
|
+
stats: Object.fromEntries(
|
|
3311
|
+
Object.entries(entry.stats).map(([k, v]) => [
|
|
3312
|
+
k,
|
|
3313
|
+
entry.gamesPlayed > 0 ? +(v / entry.gamesPlayed).toFixed(1) : 0
|
|
3314
|
+
])
|
|
3315
|
+
)
|
|
3316
|
+
}))
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
return ok(enriched);
|
|
3320
|
+
}
|
|
3321
|
+
};
|
|
3322
|
+
var AflTablesSquadSource = class {
|
|
3323
|
+
constructor(client = new AflTablesClient()) {
|
|
3324
|
+
this.client = client;
|
|
3325
|
+
}
|
|
3326
|
+
id = "afl-tables";
|
|
3327
|
+
coverage = AFL_TABLES_SQUAD_COVERAGE;
|
|
3328
|
+
async fetchSquad(query) {
|
|
3329
|
+
const competition = query.competition ?? "AFLM";
|
|
3330
|
+
const teamName = normaliseTeamName(query.team);
|
|
3331
|
+
const result = await this.client.fetchPlayerList(teamName);
|
|
3181
3332
|
if (!result.success) return result;
|
|
3333
|
+
const players = result.data.map((p) => ({
|
|
3334
|
+
playerId: p.playerId,
|
|
3335
|
+
givenName: p.givenName,
|
|
3336
|
+
surname: p.surname,
|
|
3337
|
+
displayName: p.displayName,
|
|
3338
|
+
jumperNumber: p.jumperNumber,
|
|
3339
|
+
position: p.position,
|
|
3340
|
+
dateOfBirth: p.dateOfBirth ? parseDate(p.dateOfBirth) : null,
|
|
3341
|
+
heightCm: p.heightCm,
|
|
3342
|
+
weightKg: p.weightKg,
|
|
3343
|
+
draftYear: p.draftYear,
|
|
3344
|
+
draftPosition: p.draftPosition,
|
|
3345
|
+
draftType: p.draftType,
|
|
3346
|
+
debutYear: p.debutYear,
|
|
3347
|
+
recruitedFrom: p.recruitedFrom,
|
|
3348
|
+
gamesPlayed: p.gamesPlayed,
|
|
3349
|
+
goals: p.goals
|
|
3350
|
+
}));
|
|
3182
3351
|
return ok({
|
|
3352
|
+
teamId: teamName,
|
|
3353
|
+
teamName,
|
|
3183
3354
|
season: query.season,
|
|
3184
|
-
|
|
3185
|
-
entries: transformSquiggleStandings(result.data.standings),
|
|
3355
|
+
players,
|
|
3186
3356
|
competition
|
|
3187
3357
|
});
|
|
3188
3358
|
}
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3359
|
+
};
|
|
3360
|
+
var AflTablesLadderSource = class {
|
|
3361
|
+
constructor(client = new AflTablesClient()) {
|
|
3362
|
+
this.client = client;
|
|
3363
|
+
}
|
|
3364
|
+
id = "afl-tables";
|
|
3365
|
+
coverage = AFL_TABLES_LADDER_COVERAGE;
|
|
3366
|
+
async fetchLadder(query) {
|
|
3367
|
+
const competition = query.competition ?? "AFLM";
|
|
3368
|
+
const resultsResult = await this.client.fetchSeasonResults(query.season);
|
|
3193
3369
|
if (!resultsResult.success) return resultsResult;
|
|
3194
|
-
const
|
|
3370
|
+
const entries = computeLadder(resultsResult.data, query.round ?? void 0);
|
|
3195
3371
|
return ok({
|
|
3196
3372
|
season: query.season,
|
|
3197
3373
|
roundNumber: query.round ?? null,
|
|
3198
|
-
entries
|
|
3374
|
+
entries,
|
|
3199
3375
|
competition
|
|
3200
3376
|
});
|
|
3201
3377
|
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3378
|
+
};
|
|
3379
|
+
|
|
3380
|
+
// src/sources/adapters/footywire.ts
|
|
3381
|
+
var FOOTYWIRE_MATCH_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 2010 }]]);
|
|
3382
|
+
var FOOTYWIRE_PLAYER_STATS_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 2010 }]]);
|
|
3383
|
+
var FOOTYWIRE_TEAM_STATS_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 2010 }]]);
|
|
3384
|
+
var FOOTYWIRE_SQUAD_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 2010 }]]);
|
|
3385
|
+
var FootyWireMatchSource = class {
|
|
3386
|
+
constructor(client = new FootyWireClient()) {
|
|
3387
|
+
this.client = client;
|
|
3388
|
+
}
|
|
3389
|
+
id = "footywire";
|
|
3390
|
+
coverage = FOOTYWIRE_MATCH_COVERAGE;
|
|
3391
|
+
async fetchMatches(query) {
|
|
3392
|
+
const result = await this.client.fetchSeasonFixture(query.season);
|
|
3393
|
+
if (!result.success) return result;
|
|
3394
|
+
const filtered = query.round != null ? result.data.filter((m) => m.roundNumber === query.round) : result.data;
|
|
3395
|
+
return ok(filtered);
|
|
3396
|
+
}
|
|
3397
|
+
};
|
|
3398
|
+
var FootyWirePlayerStatsSource = class {
|
|
3399
|
+
constructor(client = new FootyWireClient()) {
|
|
3400
|
+
this.client = client;
|
|
3401
|
+
}
|
|
3402
|
+
id = "footywire";
|
|
3403
|
+
coverage = FOOTYWIRE_PLAYER_STATS_COVERAGE;
|
|
3404
|
+
async fetchPlayerStats(query) {
|
|
3405
|
+
const idsResult = await this.client.fetchSeasonMatchIds(query.season);
|
|
3406
|
+
if (!idsResult.success) return idsResult;
|
|
3407
|
+
const entries = query.round != null ? idsResult.data.filter((e) => e.roundNumber === query.round) : idsResult.data;
|
|
3408
|
+
if (entries.length === 0) return ok([]);
|
|
3409
|
+
const results = await batchedMap(
|
|
3410
|
+
entries,
|
|
3411
|
+
(e) => this.client.fetchMatchPlayerStats(e.matchId, query.season, e.roundNumber),
|
|
3412
|
+
{ batchSize: 5, delayMs: 500 }
|
|
3208
3413
|
);
|
|
3414
|
+
const allStats = [];
|
|
3415
|
+
for (const result of results) {
|
|
3416
|
+
if (result.success) {
|
|
3417
|
+
allStats.push(...result.data);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
return ok(allStats);
|
|
3209
3421
|
}
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3422
|
+
};
|
|
3423
|
+
var FootyWireSquadSource = class {
|
|
3424
|
+
constructor(client = new FootyWireClient()) {
|
|
3425
|
+
this.client = client;
|
|
3426
|
+
}
|
|
3427
|
+
id = "footywire";
|
|
3428
|
+
coverage = FOOTYWIRE_SQUAD_COVERAGE;
|
|
3429
|
+
async fetchSquad(query) {
|
|
3430
|
+
const competition = query.competition ?? "AFLM";
|
|
3431
|
+
const teamName = normaliseTeamName(query.team);
|
|
3432
|
+
const result = await this.client.fetchPlayerList(teamName);
|
|
3433
|
+
if (!result.success) return result;
|
|
3434
|
+
const players = result.data.map((p) => ({
|
|
3435
|
+
playerId: p.playerId,
|
|
3436
|
+
givenName: p.givenName,
|
|
3437
|
+
surname: p.surname,
|
|
3438
|
+
displayName: p.displayName,
|
|
3439
|
+
jumperNumber: p.jumperNumber,
|
|
3440
|
+
position: p.position,
|
|
3441
|
+
dateOfBirth: p.dateOfBirth ? parseDate(p.dateOfBirth) : null,
|
|
3442
|
+
heightCm: p.heightCm,
|
|
3443
|
+
weightKg: p.weightKg,
|
|
3444
|
+
draftYear: p.draftYear,
|
|
3445
|
+
draftPosition: p.draftPosition,
|
|
3446
|
+
draftType: p.draftType,
|
|
3447
|
+
debutYear: p.debutYear,
|
|
3448
|
+
recruitedFrom: p.recruitedFrom,
|
|
3449
|
+
gamesPlayed: p.gamesPlayed,
|
|
3450
|
+
goals: p.goals
|
|
3451
|
+
}));
|
|
3452
|
+
return ok({
|
|
3453
|
+
teamId: teamName,
|
|
3454
|
+
teamName,
|
|
3455
|
+
season: query.season,
|
|
3456
|
+
players,
|
|
3457
|
+
competition
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
};
|
|
3461
|
+
var FootyWireTeamStatsSource = class {
|
|
3462
|
+
constructor(client = new FootyWireClient()) {
|
|
3463
|
+
this.client = client;
|
|
3464
|
+
}
|
|
3465
|
+
id = "footywire";
|
|
3466
|
+
coverage = FOOTYWIRE_TEAM_STATS_COVERAGE;
|
|
3467
|
+
async fetchTeamStats(query) {
|
|
3468
|
+
const summaryType = query.summaryType ?? "totals";
|
|
3469
|
+
return this.client.fetchTeamStats(query.season, summaryType);
|
|
3470
|
+
}
|
|
3471
|
+
};
|
|
3472
|
+
|
|
3473
|
+
// src/transforms/fryzigg-player-stats.ts
|
|
3474
|
+
var REQUIRED_COLUMN_GROUPS = [
|
|
3475
|
+
["match_id"],
|
|
3476
|
+
["match_date", "date"],
|
|
3477
|
+
["player_id"],
|
|
3478
|
+
["player_team", "team"]
|
|
3479
|
+
];
|
|
3480
|
+
var AFLM_COLUMNS = {
|
|
3481
|
+
date: "match_date",
|
|
3482
|
+
homeTeam: "match_home_team",
|
|
3483
|
+
awayTeam: "match_away_team",
|
|
3484
|
+
team: "player_team",
|
|
3485
|
+
round: "match_round",
|
|
3486
|
+
jumperNumber: "guernsey_number",
|
|
3487
|
+
firstName: "player_first_name",
|
|
3488
|
+
lastName: "player_last_name",
|
|
3489
|
+
playerName: void 0,
|
|
3490
|
+
freesFor: "free_kicks_for",
|
|
3491
|
+
freesAgainst: "free_kicks_against",
|
|
3492
|
+
totalClearances: "clearances",
|
|
3493
|
+
inside50s: "inside_fifties",
|
|
3494
|
+
rebound50s: "rebounds",
|
|
3495
|
+
disposalEfficiency: "disposal_efficiency_percentage",
|
|
3496
|
+
marksInside50: "marks_inside_fifty",
|
|
3497
|
+
tacklesInside50: "tackles_inside_fifty",
|
|
3498
|
+
timeOnGround: "time_on_ground_percentage",
|
|
3499
|
+
position: "player_position",
|
|
3500
|
+
dreamTeamPoints: "afl_fantasy_score",
|
|
3501
|
+
totalPossessions: void 0
|
|
3502
|
+
};
|
|
3503
|
+
var AFLW_COLUMNS = {
|
|
3504
|
+
date: "date",
|
|
3505
|
+
homeTeam: "home_team",
|
|
3506
|
+
awayTeam: "away_team",
|
|
3507
|
+
team: "team",
|
|
3508
|
+
round: "fixture_round",
|
|
3509
|
+
jumperNumber: "number",
|
|
3510
|
+
firstName: void 0,
|
|
3511
|
+
lastName: void 0,
|
|
3512
|
+
playerName: "player_name",
|
|
3513
|
+
freesFor: "frees_for",
|
|
3514
|
+
freesAgainst: "frees_against",
|
|
3515
|
+
totalClearances: "total_clearances",
|
|
3516
|
+
inside50s: "inside50s",
|
|
3517
|
+
rebound50s: "rebound50s",
|
|
3518
|
+
disposalEfficiency: "disposal_efficiency",
|
|
3519
|
+
marksInside50: "marks_inside50",
|
|
3520
|
+
tacklesInside50: "tackles_inside50",
|
|
3521
|
+
timeOnGround: "time_on_ground",
|
|
3522
|
+
position: "position",
|
|
3523
|
+
dreamTeamPoints: "fantasy_score",
|
|
3524
|
+
totalPossessions: "total_possessions"
|
|
3525
|
+
};
|
|
3526
|
+
function transformFryziggPlayerStats(frame, options) {
|
|
3527
|
+
const colIndex = /* @__PURE__ */ new Map();
|
|
3528
|
+
for (let i = 0; i < frame.names.length; i++) {
|
|
3529
|
+
const name = frame.names[i];
|
|
3530
|
+
if (name !== void 0) {
|
|
3531
|
+
colIndex.set(name, i);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
for (const group of REQUIRED_COLUMN_GROUPS) {
|
|
3535
|
+
if (!group.some((name) => colIndex.has(name))) {
|
|
3536
|
+
return err(
|
|
3537
|
+
new ScrapeError(
|
|
3538
|
+
`Fryzigg data frame missing required column: "${group.join('" or "')}"`,
|
|
3539
|
+
"fryzigg"
|
|
3540
|
+
)
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
const getCol = (name) => {
|
|
3545
|
+
if (name === void 0) return void 0;
|
|
3546
|
+
const idx = colIndex.get(name);
|
|
3547
|
+
if (idx === void 0) return void 0;
|
|
3548
|
+
return frame.columns[idx];
|
|
3549
|
+
};
|
|
3550
|
+
const isAflw = options.competition === "AFLW";
|
|
3551
|
+
const mapping = isAflw ? AFLW_COLUMNS : AFLM_COLUMNS;
|
|
3552
|
+
const cols = {
|
|
3553
|
+
matchId: getCol("match_id"),
|
|
3554
|
+
date: getCol(mapping.date),
|
|
3555
|
+
homeTeam: getCol(mapping.homeTeam),
|
|
3556
|
+
awayTeam: getCol(mapping.awayTeam),
|
|
3557
|
+
team: getCol(mapping.team),
|
|
3558
|
+
round: getCol(mapping.round),
|
|
3559
|
+
jumperNumber: getCol(mapping.jumperNumber),
|
|
3560
|
+
playerId: getCol("player_id"),
|
|
3561
|
+
firstName: getCol(mapping.firstName),
|
|
3562
|
+
lastName: getCol(mapping.lastName),
|
|
3563
|
+
playerName: getCol(mapping.playerName),
|
|
3564
|
+
kicks: getCol("kicks"),
|
|
3565
|
+
handballs: getCol("handballs"),
|
|
3566
|
+
disposals: getCol("disposals"),
|
|
3567
|
+
marks: getCol("marks"),
|
|
3568
|
+
goals: getCol("goals"),
|
|
3569
|
+
behinds: getCol("behinds"),
|
|
3570
|
+
tackles: getCol("tackles"),
|
|
3571
|
+
hitouts: getCol("hitouts"),
|
|
3572
|
+
freesFor: getCol(mapping.freesFor),
|
|
3573
|
+
freesAgainst: getCol(mapping.freesAgainst),
|
|
3574
|
+
contestedPossessions: getCol("contested_possessions"),
|
|
3575
|
+
uncontestedPossessions: getCol("uncontested_possessions"),
|
|
3576
|
+
contestedMarks: getCol("contested_marks"),
|
|
3577
|
+
intercepts: getCol("intercepts"),
|
|
3578
|
+
centreClearances: getCol("centre_clearances"),
|
|
3579
|
+
stoppageClearances: getCol("stoppage_clearances"),
|
|
3580
|
+
totalClearances: getCol(mapping.totalClearances),
|
|
3581
|
+
inside50s: getCol(mapping.inside50s),
|
|
3582
|
+
rebound50s: getCol(mapping.rebound50s),
|
|
3583
|
+
clangers: getCol("clangers"),
|
|
3584
|
+
turnovers: getCol("turnovers"),
|
|
3585
|
+
onePercenters: getCol("one_percenters"),
|
|
3586
|
+
bounces: getCol("bounces"),
|
|
3587
|
+
goalAssists: getCol("goal_assists"),
|
|
3588
|
+
disposalEfficiency: getCol(mapping.disposalEfficiency),
|
|
3589
|
+
metresGained: getCol("metres_gained"),
|
|
3590
|
+
marksInside50: getCol(mapping.marksInside50),
|
|
3591
|
+
tacklesInside50: getCol(mapping.tacklesInside50),
|
|
3592
|
+
shotsAtGoal: getCol("shots_at_goal"),
|
|
3593
|
+
scoreInvolvements: getCol("score_involvements"),
|
|
3594
|
+
totalPossessions: getCol(mapping.totalPossessions),
|
|
3595
|
+
timeOnGround: getCol(mapping.timeOnGround),
|
|
3596
|
+
ratingPoints: getCol("rating_points"),
|
|
3597
|
+
position: getCol(mapping.position),
|
|
3598
|
+
brownlowVotes: getCol("brownlow_votes"),
|
|
3599
|
+
supercoachScore: getCol("supercoach_score"),
|
|
3600
|
+
dreamTeamPoints: getCol(mapping.dreamTeamPoints),
|
|
3601
|
+
effectiveDisposals: getCol("effective_disposals"),
|
|
3602
|
+
effectiveKicks: getCol("effective_kicks"),
|
|
3603
|
+
pressureActs: getCol("pressure_acts"),
|
|
3604
|
+
defHalfPressureActs: getCol("def_half_pressure_acts"),
|
|
3605
|
+
spoils: getCol("spoils"),
|
|
3606
|
+
hitoutsToAdvantage: getCol("hitouts_to_advantage"),
|
|
3607
|
+
hitoutWinPercentage: getCol("hitout_win_percentage"),
|
|
3608
|
+
groundBallGets: getCol("ground_ball_gets"),
|
|
3609
|
+
f50GroundBallGets: getCol("f50_ground_ball_gets"),
|
|
3610
|
+
interceptMarks: getCol("intercept_marks"),
|
|
3611
|
+
marksOnLead: getCol("marks_on_lead"),
|
|
3612
|
+
contestOffOneOnOnes: getCol("contest_off_one_on_ones"),
|
|
3613
|
+
contestOffWins: getCol("contest_off_wins"),
|
|
3614
|
+
contestDefOneOnOnes: getCol("contest_def_one_on_ones"),
|
|
3615
|
+
contestDefLosses: getCol("contest_def_losses"),
|
|
3616
|
+
ruckContests: getCol("ruck_contests"),
|
|
3617
|
+
scoreLaunches: getCol("score_launches")
|
|
3618
|
+
};
|
|
3619
|
+
const dateCol = cols.date;
|
|
3620
|
+
const roundCol = cols.round;
|
|
3621
|
+
const nRows = dateCol ? dateCol.length : 0;
|
|
3622
|
+
const hasFilters = options.season !== void 0 || options.round !== void 0;
|
|
3623
|
+
let rowIndices = null;
|
|
3624
|
+
let rowCount = nRows;
|
|
3625
|
+
if (hasFilters) {
|
|
3626
|
+
const matching = [];
|
|
3627
|
+
for (let i = 0; i < nRows; i++) {
|
|
3628
|
+
if (options.season !== void 0) {
|
|
3629
|
+
const dateStr = dateCol?.[i];
|
|
3630
|
+
if (typeof dateStr !== "string") continue;
|
|
3631
|
+
const year = Number(dateStr.slice(0, 4));
|
|
3632
|
+
if (year !== options.season) continue;
|
|
3633
|
+
}
|
|
3634
|
+
if (options.round !== void 0 && roundCol) {
|
|
3635
|
+
const roundVal = roundCol[i];
|
|
3636
|
+
const roundNum = typeof roundVal === "string" ? Number(roundVal) : roundVal;
|
|
3637
|
+
if (roundNum !== options.round) continue;
|
|
3638
|
+
}
|
|
3639
|
+
matching.push(i);
|
|
3220
3640
|
}
|
|
3641
|
+
rowIndices = matching;
|
|
3642
|
+
rowCount = matching.length;
|
|
3643
|
+
}
|
|
3644
|
+
const stats = new Array(rowCount);
|
|
3645
|
+
for (let j = 0; j < rowCount; j++) {
|
|
3646
|
+
const i = rowIndices ? rowIndices[j] : j;
|
|
3647
|
+
stats[j] = mapRow(i, cols, isAflw, options.competition);
|
|
3648
|
+
}
|
|
3649
|
+
return ok(stats);
|
|
3650
|
+
}
|
|
3651
|
+
function numAt(column, i) {
|
|
3652
|
+
if (!column) return null;
|
|
3653
|
+
const v = column[i];
|
|
3654
|
+
return typeof v === "number" ? v : null;
|
|
3655
|
+
}
|
|
3656
|
+
function strAt(column, i) {
|
|
3657
|
+
if (!column) return null;
|
|
3658
|
+
const v = column[i];
|
|
3659
|
+
return typeof v === "string" ? v : null;
|
|
3660
|
+
}
|
|
3661
|
+
function roundAt(column, i) {
|
|
3662
|
+
if (!column) return 0;
|
|
3663
|
+
const v = column[i];
|
|
3664
|
+
if (typeof v === "number") return v;
|
|
3665
|
+
if (typeof v === "string") {
|
|
3666
|
+
const n = Number(v);
|
|
3667
|
+
return Number.isNaN(n) ? 0 : n;
|
|
3221
3668
|
}
|
|
3222
|
-
|
|
3223
|
-
if (!ladderResult.success) return ladderResult;
|
|
3224
|
-
const firstLadder = ladderResult.data.ladders[0];
|
|
3225
|
-
const entries = firstLadder ? transformLadderEntries(firstLadder.entries) : [];
|
|
3226
|
-
return ok({
|
|
3227
|
-
season: query.season,
|
|
3228
|
-
roundNumber: ladderResult.data.round?.roundNumber ?? null,
|
|
3229
|
-
entries,
|
|
3230
|
-
competition
|
|
3231
|
-
});
|
|
3669
|
+
return 0;
|
|
3232
3670
|
}
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
|
|
3250
|
-
jumperNumber: p.jumperNumber ?? null,
|
|
3251
|
-
position,
|
|
3252
|
-
isEmergency: position !== null && EMERGENCY_POSITIONS.has(position),
|
|
3253
|
-
isSubstitute: position !== null && SUBSTITUTE_POSITIONS.has(position)
|
|
3254
|
-
};
|
|
3255
|
-
});
|
|
3671
|
+
function mapRow(i, c, isAflw, competition) {
|
|
3672
|
+
const dateStr = strAt(c.date, i);
|
|
3673
|
+
let firstName;
|
|
3674
|
+
let lastName;
|
|
3675
|
+
if (isAflw) {
|
|
3676
|
+
const playerName = strAt(c.playerName, i) ?? "";
|
|
3677
|
+
const commaIdx = playerName.indexOf(", ");
|
|
3678
|
+
firstName = commaIdx >= 0 ? playerName.slice(0, commaIdx) : playerName;
|
|
3679
|
+
lastName = commaIdx >= 0 ? playerName.slice(commaIdx + 2) : "";
|
|
3680
|
+
} else {
|
|
3681
|
+
firstName = strAt(c.firstName, i) ?? "";
|
|
3682
|
+
lastName = strAt(c.lastName, i) ?? "";
|
|
3683
|
+
}
|
|
3684
|
+
const team = strAt(c.team, i) ?? "";
|
|
3685
|
+
const homeTeam = strAt(c.homeTeam, i);
|
|
3686
|
+
const awayTeam = strAt(c.awayTeam, i);
|
|
3256
3687
|
return {
|
|
3257
|
-
matchId:
|
|
3258
|
-
season,
|
|
3259
|
-
roundNumber,
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3688
|
+
matchId: String(c.matchId?.[i] ?? ""),
|
|
3689
|
+
season: dateStr ? Number(dateStr.slice(0, 4)) : 0,
|
|
3690
|
+
roundNumber: roundAt(c.round, i),
|
|
3691
|
+
team: normaliseTeamName(team),
|
|
3692
|
+
competition,
|
|
3693
|
+
date: dateStr ? parseDate(dateStr) : null,
|
|
3694
|
+
homeTeam: homeTeam ? normaliseTeamName(homeTeam) : null,
|
|
3695
|
+
awayTeam: awayTeam ? normaliseTeamName(awayTeam) : null,
|
|
3696
|
+
playerId: String(c.playerId?.[i] ?? ""),
|
|
3697
|
+
givenName: firstName,
|
|
3698
|
+
surname: lastName,
|
|
3699
|
+
displayName: `${firstName} ${lastName}`.trim(),
|
|
3700
|
+
jumperNumber: numAt(c.jumperNumber, i),
|
|
3701
|
+
kicks: numAt(c.kicks, i),
|
|
3702
|
+
handballs: numAt(c.handballs, i),
|
|
3703
|
+
disposals: numAt(c.disposals, i),
|
|
3704
|
+
marks: numAt(c.marks, i),
|
|
3705
|
+
goals: numAt(c.goals, i),
|
|
3706
|
+
behinds: numAt(c.behinds, i),
|
|
3707
|
+
tackles: numAt(c.tackles, i),
|
|
3708
|
+
hitouts: numAt(c.hitouts, i),
|
|
3709
|
+
freesFor: numAt(c.freesFor, i),
|
|
3710
|
+
freesAgainst: numAt(c.freesAgainst, i),
|
|
3711
|
+
contestedPossessions: numAt(c.contestedPossessions, i),
|
|
3712
|
+
uncontestedPossessions: numAt(c.uncontestedPossessions, i),
|
|
3713
|
+
contestedMarks: numAt(c.contestedMarks, i),
|
|
3714
|
+
intercepts: numAt(c.intercepts, i),
|
|
3715
|
+
centreClearances: numAt(c.centreClearances, i),
|
|
3716
|
+
stoppageClearances: numAt(c.stoppageClearances, i),
|
|
3717
|
+
totalClearances: numAt(c.totalClearances, i),
|
|
3718
|
+
inside50s: numAt(c.inside50s, i),
|
|
3719
|
+
rebound50s: numAt(c.rebound50s, i),
|
|
3720
|
+
clangers: numAt(c.clangers, i),
|
|
3721
|
+
turnovers: numAt(c.turnovers, i),
|
|
3722
|
+
onePercenters: numAt(c.onePercenters, i),
|
|
3723
|
+
bounces: numAt(c.bounces, i),
|
|
3724
|
+
goalAssists: numAt(c.goalAssists, i),
|
|
3725
|
+
disposalEfficiency: numAt(c.disposalEfficiency, i),
|
|
3726
|
+
metresGained: numAt(c.metresGained, i),
|
|
3727
|
+
goalAccuracy: null,
|
|
3728
|
+
marksInside50: numAt(c.marksInside50, i),
|
|
3729
|
+
tacklesInside50: numAt(c.tacklesInside50, i),
|
|
3730
|
+
shotsAtGoal: numAt(c.shotsAtGoal, i),
|
|
3731
|
+
scoreInvolvements: numAt(c.scoreInvolvements, i),
|
|
3732
|
+
totalPossessions: numAt(c.totalPossessions, i),
|
|
3733
|
+
timeOnGroundPercentage: numAt(c.timeOnGround, i),
|
|
3734
|
+
ratingPoints: numAt(c.ratingPoints, i),
|
|
3735
|
+
position: strAt(c.position, i),
|
|
3736
|
+
goalEfficiency: null,
|
|
3737
|
+
shotEfficiency: null,
|
|
3738
|
+
interchangeCounts: null,
|
|
3739
|
+
brownlowVotes: numAt(c.brownlowVotes, i),
|
|
3740
|
+
supercoachScore: numAt(c.supercoachScore, i),
|
|
3741
|
+
dreamTeamPoints: numAt(c.dreamTeamPoints, i),
|
|
3742
|
+
effectiveDisposals: numAt(c.effectiveDisposals, i),
|
|
3743
|
+
effectiveKicks: numAt(c.effectiveKicks, i),
|
|
3744
|
+
kickEfficiency: null,
|
|
3745
|
+
kickToHandballRatio: null,
|
|
3746
|
+
pressureActs: numAt(c.pressureActs, i),
|
|
3747
|
+
defHalfPressureActs: numAt(c.defHalfPressureActs, i),
|
|
3748
|
+
spoils: numAt(c.spoils, i),
|
|
3749
|
+
hitoutsToAdvantage: numAt(c.hitoutsToAdvantage, i),
|
|
3750
|
+
hitoutWinPercentage: numAt(c.hitoutWinPercentage, i),
|
|
3751
|
+
hitoutToAdvantageRate: null,
|
|
3752
|
+
groundBallGets: numAt(c.groundBallGets, i),
|
|
3753
|
+
f50GroundBallGets: numAt(c.f50GroundBallGets, i),
|
|
3754
|
+
interceptMarks: numAt(c.interceptMarks, i),
|
|
3755
|
+
marksOnLead: numAt(c.marksOnLead, i),
|
|
3756
|
+
contestedPossessionRate: null,
|
|
3757
|
+
contestOffOneOnOnes: numAt(c.contestOffOneOnOnes, i),
|
|
3758
|
+
contestOffWins: numAt(c.contestOffWins, i),
|
|
3759
|
+
contestOffWinsPercentage: null,
|
|
3760
|
+
contestDefOneOnOnes: numAt(c.contestDefOneOnOnes, i),
|
|
3761
|
+
contestDefLosses: numAt(c.contestDefLosses, i),
|
|
3762
|
+
contestDefLossPercentage: null,
|
|
3763
|
+
centreBounceAttendances: null,
|
|
3764
|
+
kickins: null,
|
|
3765
|
+
kickinsPlayon: null,
|
|
3766
|
+
ruckContests: numAt(c.ruckContests, i),
|
|
3767
|
+
scoreLaunches: numAt(c.scoreLaunches, i),
|
|
3768
|
+
source: "fryzigg"
|
|
3265
3769
|
};
|
|
3266
3770
|
}
|
|
3267
3771
|
|
|
3268
|
-
// src/
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
const client = new AflApiClient();
|
|
3280
|
-
if (query.matchId) {
|
|
3281
|
-
const rosterResult = await client.fetchMatchRoster(query.matchId);
|
|
3282
|
-
if (!rosterResult.success) return rosterResult;
|
|
3283
|
-
return ok([transformMatchRoster(rosterResult.data, query.season, query.round, competition)]);
|
|
3284
|
-
}
|
|
3285
|
-
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
3286
|
-
if (!seasonResult.success) return seasonResult;
|
|
3287
|
-
const matchItems = await client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round);
|
|
3288
|
-
if (!matchItems.success) return matchItems;
|
|
3289
|
-
if (matchItems.data.length === 0) {
|
|
3290
|
-
return err(new AflApiError(`No matches found for round ${query.round}`));
|
|
3291
|
-
}
|
|
3292
|
-
const rosterResults = await batchedMap(
|
|
3293
|
-
matchItems.data,
|
|
3294
|
-
(item) => client.fetchMatchRoster(item.match.matchId)
|
|
3295
|
-
);
|
|
3296
|
-
const lineups = [];
|
|
3297
|
-
for (const rosterResult of rosterResults) {
|
|
3298
|
-
if (!rosterResult.success) return rosterResult;
|
|
3299
|
-
lineups.push(transformMatchRoster(rosterResult.data, query.season, query.round, competition));
|
|
3772
|
+
// src/sources/fryzigg.ts
|
|
3773
|
+
import { isDataFrame, parseRds, RdsError } from "@jackemcpherson/rds-js";
|
|
3774
|
+
var FRYZIGG_URLS = {
|
|
3775
|
+
AFLM: "http://www.fryziggafl.net/static/fryziggafl.rds",
|
|
3776
|
+
AFLW: "http://www.fryziggafl.net/static/aflw_player_stats.rds"
|
|
3777
|
+
};
|
|
3778
|
+
var USER_AGENT2 = "fitzRoy-ts/1.0 (https://github.com/jackemcpherson/fitzRoy-ts)";
|
|
3779
|
+
var FryziggClient = class {
|
|
3780
|
+
fetchFn;
|
|
3781
|
+
constructor(options) {
|
|
3782
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
3300
3783
|
}
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3784
|
+
/**
|
|
3785
|
+
* Fetch the full player statistics dataset for a competition.
|
|
3786
|
+
*
|
|
3787
|
+
* Returns column-major DataFrame from rds-js. The caller is responsible
|
|
3788
|
+
* for filtering rows and mapping to domain types.
|
|
3789
|
+
*
|
|
3790
|
+
* @param competition - AFLM or AFLW.
|
|
3791
|
+
* @returns Column-major DataFrame with all rows, or an error.
|
|
3792
|
+
*/
|
|
3793
|
+
async fetchPlayerStats(competition) {
|
|
3794
|
+
const url = FRYZIGG_URLS[competition];
|
|
3795
|
+
if (!url) {
|
|
3796
|
+
return err(new ScrapeError(`Fryzigg does not publish ${competition} data`, "fryzigg"));
|
|
3797
|
+
}
|
|
3798
|
+
try {
|
|
3799
|
+
const response = await this.fetchFn(url, {
|
|
3800
|
+
headers: { "User-Agent": USER_AGENT2 }
|
|
3801
|
+
});
|
|
3802
|
+
if (!response.ok) {
|
|
3803
|
+
return err(
|
|
3804
|
+
new ScrapeError(`Fryzigg request failed: ${response.status} (${url})`, "fryzigg")
|
|
3316
3805
|
);
|
|
3317
|
-
if (!itemsResult2.success) return itemsResult2;
|
|
3318
|
-
return ok(transformMatchItems(itemsResult2.data, query.season, competition));
|
|
3319
3806
|
}
|
|
3320
|
-
const
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
case "footywire": {
|
|
3325
|
-
if (competition === "AFLW") return err(aflwUnsupportedError("footywire"));
|
|
3326
|
-
const client = new FootyWireClient();
|
|
3327
|
-
const result = await client.fetchSeasonResults(query.season);
|
|
3328
|
-
if (!result.success) return result;
|
|
3329
|
-
if (query.round != null) {
|
|
3330
|
-
return ok(result.data.filter((m) => m.roundNumber === query.round));
|
|
3807
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
3808
|
+
const result = await parseRds(buffer);
|
|
3809
|
+
if (!isDataFrame(result)) {
|
|
3810
|
+
return err(new ScrapeError("Fryzigg RDS file did not contain a data frame", "fryzigg"));
|
|
3331
3811
|
}
|
|
3332
|
-
return result;
|
|
3333
|
-
}
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
const client = new AflTablesClient();
|
|
3337
|
-
const result = await client.fetchSeasonResults(query.season);
|
|
3338
|
-
if (!result.success) return result;
|
|
3339
|
-
if (query.round != null) {
|
|
3340
|
-
return ok(result.data.filter((m) => m.roundNumber === query.round));
|
|
3812
|
+
return ok(result);
|
|
3813
|
+
} catch (cause) {
|
|
3814
|
+
if (cause instanceof RdsError) {
|
|
3815
|
+
return err(new ScrapeError(`Fryzigg RDS parse error: ${cause.message}`, "fryzigg"));
|
|
3341
3816
|
}
|
|
3342
|
-
return
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
if (!result.success) return result;
|
|
3349
|
-
return ok(transformSquiggleGamesToResults(result.data.games, query.season));
|
|
3817
|
+
return err(
|
|
3818
|
+
new ScrapeError(
|
|
3819
|
+
`Fryzigg request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
3820
|
+
"fryzigg"
|
|
3821
|
+
)
|
|
3822
|
+
);
|
|
3350
3823
|
}
|
|
3351
|
-
default:
|
|
3352
|
-
return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
|
|
3353
3824
|
}
|
|
3354
|
-
}
|
|
3825
|
+
};
|
|
3355
3826
|
|
|
3356
|
-
// src/
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
return err(new ValidationError(`Team not found: ${teamName}`));
|
|
3827
|
+
// src/sources/adapters/fryzigg.ts
|
|
3828
|
+
var FRYZIGG_PLAYER_STATS_COVERAGE = /* @__PURE__ */ new Map([
|
|
3829
|
+
["AFLM", { minSeason: 2012 }],
|
|
3830
|
+
["AFLW", { minSeason: 2017 }]
|
|
3831
|
+
]);
|
|
3832
|
+
var FryziggPlayerStatsSource = class {
|
|
3833
|
+
constructor(client = new FryziggClient()) {
|
|
3834
|
+
this.client = client;
|
|
3365
3835
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3836
|
+
id = "fryzigg";
|
|
3837
|
+
coverage = FRYZIGG_PLAYER_STATS_COVERAGE;
|
|
3838
|
+
async fetchPlayerStats(query) {
|
|
3839
|
+
const competition = query.competition ?? "AFLM";
|
|
3840
|
+
const result = await this.client.fetchPlayerStats(competition);
|
|
3841
|
+
if (!result.success) return result;
|
|
3842
|
+
return transformFryziggPlayerStats(result.data, {
|
|
3843
|
+
competition,
|
|
3844
|
+
season: query.season,
|
|
3845
|
+
round: query.round
|
|
3846
|
+
});
|
|
3847
|
+
}
|
|
3848
|
+
};
|
|
3849
|
+
|
|
3850
|
+
// src/sources/adapters/registry.ts
|
|
3851
|
+
var CapabilityRegistry = class {
|
|
3852
|
+
constructor(defaultSource) {
|
|
3853
|
+
this.defaultSource = defaultSource;
|
|
3854
|
+
}
|
|
3855
|
+
adapters = /* @__PURE__ */ new Map();
|
|
3856
|
+
register(adapter) {
|
|
3857
|
+
this.adapters.set(adapter.id, adapter);
|
|
3858
|
+
}
|
|
3859
|
+
get(id) {
|
|
3860
|
+
return this.adapters.get(id);
|
|
3861
|
+
}
|
|
3862
|
+
list() {
|
|
3863
|
+
return [...this.adapters.keys()];
|
|
3864
|
+
}
|
|
3865
|
+
all() {
|
|
3866
|
+
return [...this.adapters.values()];
|
|
3867
|
+
}
|
|
3868
|
+
};
|
|
3869
|
+
var matchRegistry = new CapabilityRegistry("afl-api");
|
|
3870
|
+
var playerStatsRegistry = new CapabilityRegistry("afl-api");
|
|
3871
|
+
var teamStatsRegistry = new CapabilityRegistry("afl-tables");
|
|
3872
|
+
var squadRegistry = new CapabilityRegistry("afl-api");
|
|
3873
|
+
var lineupRegistry = new CapabilityRegistry("afl-api");
|
|
3874
|
+
var ladderRegistry = new CapabilityRegistry("afl-api");
|
|
3875
|
+
|
|
3876
|
+
// src/transforms/squiggle.ts
|
|
3877
|
+
function toMatchStatus2(complete) {
|
|
3878
|
+
if (complete === 100) return "Complete";
|
|
3879
|
+
if (complete > 0) return "Live";
|
|
3880
|
+
return "Upcoming";
|
|
3391
3881
|
}
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3882
|
+
function transformSquiggleGamesToResults(games, season) {
|
|
3883
|
+
return games.filter((g) => g.complete === 100).map((g) => ({
|
|
3884
|
+
matchId: `SQ_${g.id}`,
|
|
3885
|
+
season,
|
|
3886
|
+
roundNumber: g.round,
|
|
3887
|
+
roundType: inferRoundType(g.roundname),
|
|
3888
|
+
roundName: g.roundname || null,
|
|
3889
|
+
date: parseDate(g.unixtime) ?? new Date(g.unixtime * 1e3),
|
|
3890
|
+
venue: normaliseVenueName(g.venue),
|
|
3891
|
+
homeTeam: normaliseTeamName(g.hteam),
|
|
3892
|
+
awayTeam: normaliseTeamName(g.ateam),
|
|
3893
|
+
homeGoals: g.hgoals ?? 0,
|
|
3894
|
+
homeBehinds: g.hbehinds ?? 0,
|
|
3895
|
+
homePoints: g.hscore ?? 0,
|
|
3896
|
+
awayGoals: g.agoals ?? 0,
|
|
3897
|
+
awayBehinds: g.abehinds ?? 0,
|
|
3898
|
+
awayPoints: g.ascore ?? 0,
|
|
3899
|
+
margin: (g.hscore ?? 0) - (g.ascore ?? 0),
|
|
3900
|
+
q1Home: null,
|
|
3901
|
+
q2Home: null,
|
|
3902
|
+
q3Home: null,
|
|
3903
|
+
q4Home: null,
|
|
3904
|
+
q1Away: null,
|
|
3905
|
+
q2Away: null,
|
|
3906
|
+
q3Away: null,
|
|
3907
|
+
q4Away: null,
|
|
3908
|
+
status: "Complete",
|
|
3909
|
+
attendance: null,
|
|
3910
|
+
weatherTempCelsius: null,
|
|
3911
|
+
weatherType: null,
|
|
3912
|
+
roundCode: toRoundCode(g.roundname),
|
|
3913
|
+
venueState: null,
|
|
3914
|
+
venueTimezone: g.tz || null,
|
|
3915
|
+
homeRushedBehinds: null,
|
|
3916
|
+
awayRushedBehinds: null,
|
|
3917
|
+
homeMinutesInFront: null,
|
|
3918
|
+
awayMinutesInFront: null,
|
|
3919
|
+
source: "squiggle",
|
|
3920
|
+
competition: "AFLM"
|
|
3415
3921
|
}));
|
|
3416
|
-
const results = await batchedMap(
|
|
3417
|
-
teamEntries,
|
|
3418
|
-
(entry) => client.fetchSquad(entry.id, seasonResult.data)
|
|
3419
|
-
);
|
|
3420
|
-
const allPlayers = [];
|
|
3421
|
-
for (let i = 0; i < results.length; i++) {
|
|
3422
|
-
const result = results[i];
|
|
3423
|
-
const entry = teamEntries[i];
|
|
3424
|
-
if (result?.success && entry) {
|
|
3425
|
-
allPlayers.push(...mapSquadToPlayerDetails(result.data, entry.name, competition));
|
|
3426
|
-
}
|
|
3427
|
-
}
|
|
3428
|
-
return ok(allPlayers);
|
|
3429
3922
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3923
|
+
function transformSquiggleGamesToFixture(games, season) {
|
|
3924
|
+
return games.map((g) => {
|
|
3925
|
+
const status = toMatchStatus2(g.complete);
|
|
3926
|
+
const isComplete = status === "Complete";
|
|
3927
|
+
return {
|
|
3928
|
+
matchId: `SQ_${g.id}`,
|
|
3929
|
+
season,
|
|
3930
|
+
roundNumber: g.round,
|
|
3931
|
+
roundType: inferRoundType(g.roundname),
|
|
3932
|
+
roundName: g.roundname || null,
|
|
3933
|
+
date: parseDate(g.unixtime) ?? new Date(g.unixtime * 1e3),
|
|
3934
|
+
venue: normaliseVenueName(g.venue),
|
|
3935
|
+
homeTeam: normaliseTeamName(g.hteam),
|
|
3936
|
+
awayTeam: normaliseTeamName(g.ateam),
|
|
3937
|
+
homeGoals: isComplete ? g.hgoals ?? 0 : null,
|
|
3938
|
+
homeBehinds: isComplete ? g.hbehinds ?? 0 : null,
|
|
3939
|
+
homePoints: isComplete ? g.hscore ?? 0 : null,
|
|
3940
|
+
awayGoals: isComplete ? g.agoals ?? 0 : null,
|
|
3941
|
+
awayBehinds: isComplete ? g.abehinds ?? 0 : null,
|
|
3942
|
+
awayPoints: isComplete ? g.ascore ?? 0 : null,
|
|
3943
|
+
margin: isComplete ? (g.hscore ?? 0) - (g.ascore ?? 0) : null,
|
|
3944
|
+
q1Home: null,
|
|
3945
|
+
q2Home: null,
|
|
3946
|
+
q3Home: null,
|
|
3947
|
+
q4Home: null,
|
|
3948
|
+
q1Away: null,
|
|
3949
|
+
q2Away: null,
|
|
3950
|
+
q3Away: null,
|
|
3951
|
+
q4Away: null,
|
|
3952
|
+
status,
|
|
3953
|
+
attendance: null,
|
|
3954
|
+
weatherTempCelsius: null,
|
|
3955
|
+
weatherType: null,
|
|
3956
|
+
roundCode: toRoundCode(g.roundname),
|
|
3957
|
+
venueState: null,
|
|
3958
|
+
venueTimezone: g.tz || null,
|
|
3959
|
+
homeRushedBehinds: null,
|
|
3960
|
+
awayRushedBehinds: null,
|
|
3961
|
+
homeMinutesInFront: null,
|
|
3962
|
+
awayMinutesInFront: null,
|
|
3963
|
+
source: "squiggle",
|
|
3964
|
+
competition: "AFLM"
|
|
3965
|
+
};
|
|
3966
|
+
});
|
|
3468
3967
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
);
|
|
3484
|
-
}
|
|
3968
|
+
function transformSquiggleStandings(standings) {
|
|
3969
|
+
return standings.map((s) => ({
|
|
3970
|
+
position: s.rank,
|
|
3971
|
+
team: normaliseTeamName(s.name),
|
|
3972
|
+
played: s.played,
|
|
3973
|
+
wins: s.wins,
|
|
3974
|
+
losses: s.losses,
|
|
3975
|
+
draws: s.draws,
|
|
3976
|
+
pointsFor: s.for,
|
|
3977
|
+
pointsAgainst: s.against,
|
|
3978
|
+
percentage: s.percentage,
|
|
3979
|
+
premiershipsPoints: s.pts,
|
|
3980
|
+
form: null
|
|
3981
|
+
}));
|
|
3485
3982
|
}
|
|
3486
3983
|
|
|
3487
|
-
// src/
|
|
3488
|
-
import {
|
|
3489
|
-
var
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3984
|
+
// src/lib/squiggle-validation.ts
|
|
3985
|
+
import { z as z2 } from "zod";
|
|
3986
|
+
var SquiggleGameSchema = z2.object({
|
|
3987
|
+
id: z2.number(),
|
|
3988
|
+
year: z2.number(),
|
|
3989
|
+
round: z2.number(),
|
|
3990
|
+
roundname: z2.string(),
|
|
3991
|
+
hteam: z2.string(),
|
|
3992
|
+
ateam: z2.string(),
|
|
3993
|
+
hteamid: z2.number(),
|
|
3994
|
+
ateamid: z2.number(),
|
|
3995
|
+
hscore: z2.number().nullable(),
|
|
3996
|
+
ascore: z2.number().nullable(),
|
|
3997
|
+
hgoals: z2.number().nullable(),
|
|
3998
|
+
agoals: z2.number().nullable(),
|
|
3999
|
+
hbehinds: z2.number().nullable(),
|
|
4000
|
+
abehinds: z2.number().nullable(),
|
|
4001
|
+
winner: z2.string().nullable(),
|
|
4002
|
+
winnerteamid: z2.number().nullable(),
|
|
4003
|
+
venue: z2.string(),
|
|
4004
|
+
date: z2.string(),
|
|
4005
|
+
localtime: z2.string(),
|
|
4006
|
+
tz: z2.string(),
|
|
4007
|
+
unixtime: z2.number(),
|
|
4008
|
+
timestr: z2.string().nullable(),
|
|
4009
|
+
complete: z2.number(),
|
|
4010
|
+
is_final: z2.number(),
|
|
4011
|
+
is_grand_final: z2.number(),
|
|
4012
|
+
updated: z2.string()
|
|
4013
|
+
});
|
|
4014
|
+
var SquiggleGamesResponseSchema = z2.object({
|
|
4015
|
+
games: z2.array(SquiggleGameSchema)
|
|
4016
|
+
});
|
|
4017
|
+
var SquiggleStandingSchema = z2.object({
|
|
4018
|
+
id: z2.number(),
|
|
4019
|
+
name: z2.string(),
|
|
4020
|
+
rank: z2.number(),
|
|
4021
|
+
played: z2.number(),
|
|
4022
|
+
wins: z2.number(),
|
|
4023
|
+
losses: z2.number(),
|
|
4024
|
+
draws: z2.number(),
|
|
4025
|
+
pts: z2.number(),
|
|
4026
|
+
for: z2.number(),
|
|
4027
|
+
against: z2.number(),
|
|
4028
|
+
percentage: z2.number(),
|
|
4029
|
+
goals_for: z2.number(),
|
|
4030
|
+
goals_against: z2.number(),
|
|
4031
|
+
behinds_for: z2.number(),
|
|
4032
|
+
behinds_against: z2.number()
|
|
4033
|
+
});
|
|
4034
|
+
var SquiggleStandingsResponseSchema = z2.object({
|
|
4035
|
+
standings: z2.array(SquiggleStandingSchema)
|
|
4036
|
+
});
|
|
4037
|
+
|
|
4038
|
+
// src/sources/squiggle.ts
|
|
4039
|
+
var SQUIGGLE_BASE = "https://api.squiggle.com.au/";
|
|
3493
4040
|
var USER_AGENT3 = "fitzRoy-ts/1.0 (https://github.com/jackemcpherson/fitzRoy-ts)";
|
|
3494
|
-
var
|
|
4041
|
+
var SquiggleClient = class {
|
|
3495
4042
|
fetchFn;
|
|
3496
4043
|
constructor(options) {
|
|
3497
4044
|
this.fetchFn = options?.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
3498
4045
|
}
|
|
3499
4046
|
/**
|
|
3500
|
-
* Fetch
|
|
3501
|
-
*
|
|
3502
|
-
* Returns column-major DataFrame from rds-js. The caller is responsible
|
|
3503
|
-
* for filtering rows and mapping to domain types.
|
|
3504
|
-
*
|
|
3505
|
-
* @param competition - AFLM or AFLW.
|
|
3506
|
-
* @returns Column-major DataFrame with all rows, or an error.
|
|
4047
|
+
* Fetch JSON from the Squiggle API.
|
|
3507
4048
|
*/
|
|
3508
|
-
async
|
|
3509
|
-
const url =
|
|
4049
|
+
async fetchJson(params) {
|
|
4050
|
+
const url = `${SQUIGGLE_BASE}?${params.toString()}`;
|
|
3510
4051
|
try {
|
|
3511
4052
|
const response = await this.fetchFn(url, {
|
|
3512
4053
|
headers: { "User-Agent": USER_AGENT3 }
|
|
3513
4054
|
});
|
|
3514
4055
|
if (!response.ok) {
|
|
3515
4056
|
return err(
|
|
3516
|
-
new ScrapeError(`
|
|
4057
|
+
new ScrapeError(`Squiggle request failed: ${response.status} (${url})`, "squiggle")
|
|
3517
4058
|
);
|
|
3518
4059
|
}
|
|
3519
|
-
const
|
|
3520
|
-
|
|
3521
|
-
if (!isDataFrame(result)) {
|
|
3522
|
-
return err(new ScrapeError("Fryzigg RDS file did not contain a data frame", "fryzigg"));
|
|
3523
|
-
}
|
|
3524
|
-
return ok(result);
|
|
4060
|
+
const json = await response.json();
|
|
4061
|
+
return ok(json);
|
|
3525
4062
|
} catch (cause) {
|
|
3526
|
-
if (cause instanceof RdsError) {
|
|
3527
|
-
return err(new ScrapeError(`Fryzigg RDS parse error: ${cause.message}`, "fryzigg"));
|
|
3528
|
-
}
|
|
3529
4063
|
return err(
|
|
3530
4064
|
new ScrapeError(
|
|
3531
|
-
`
|
|
3532
|
-
"
|
|
4065
|
+
`Squiggle request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
4066
|
+
"squiggle"
|
|
3533
4067
|
)
|
|
3534
4068
|
);
|
|
3535
4069
|
}
|
|
3536
4070
|
}
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
playerName: void 0,
|
|
3556
|
-
freesFor: "free_kicks_for",
|
|
3557
|
-
freesAgainst: "free_kicks_against",
|
|
3558
|
-
totalClearances: "clearances",
|
|
3559
|
-
inside50s: "inside_fifties",
|
|
3560
|
-
rebound50s: "rebounds",
|
|
3561
|
-
disposalEfficiency: "disposal_efficiency_percentage",
|
|
3562
|
-
marksInside50: "marks_inside_fifty",
|
|
3563
|
-
tacklesInside50: "tackles_inside_fifty",
|
|
3564
|
-
timeOnGround: "time_on_ground_percentage",
|
|
3565
|
-
position: "player_position",
|
|
3566
|
-
dreamTeamPoints: "afl_fantasy_score",
|
|
3567
|
-
totalPossessions: void 0
|
|
3568
|
-
};
|
|
3569
|
-
var AFLW_COLUMNS = {
|
|
3570
|
-
date: "date",
|
|
3571
|
-
homeTeam: "home_team",
|
|
3572
|
-
awayTeam: "away_team",
|
|
3573
|
-
team: "team",
|
|
3574
|
-
round: "fixture_round",
|
|
3575
|
-
jumperNumber: "number",
|
|
3576
|
-
firstName: void 0,
|
|
3577
|
-
lastName: void 0,
|
|
3578
|
-
playerName: "player_name",
|
|
3579
|
-
freesFor: "frees_for",
|
|
3580
|
-
freesAgainst: "frees_against",
|
|
3581
|
-
totalClearances: "total_clearances",
|
|
3582
|
-
inside50s: "inside50s",
|
|
3583
|
-
rebound50s: "rebound50s",
|
|
3584
|
-
disposalEfficiency: "disposal_efficiency",
|
|
3585
|
-
marksInside50: "marks_inside50",
|
|
3586
|
-
tacklesInside50: "tackles_inside50",
|
|
3587
|
-
timeOnGround: "time_on_ground",
|
|
3588
|
-
position: "position",
|
|
3589
|
-
dreamTeamPoints: "fantasy_score",
|
|
3590
|
-
totalPossessions: "total_possessions"
|
|
3591
|
-
};
|
|
3592
|
-
function transformFryziggPlayerStats(frame, options) {
|
|
3593
|
-
const colIndex = /* @__PURE__ */ new Map();
|
|
3594
|
-
for (let i = 0; i < frame.names.length; i++) {
|
|
3595
|
-
const name = frame.names[i];
|
|
3596
|
-
if (name !== void 0) {
|
|
3597
|
-
colIndex.set(name, i);
|
|
4071
|
+
/**
|
|
4072
|
+
* Fetch games (match results or fixture) from the Squiggle API.
|
|
4073
|
+
*
|
|
4074
|
+
* @param year - Season year.
|
|
4075
|
+
* @param round - Optional round number.
|
|
4076
|
+
* @param complete - Optional completion filter (100 = complete, omit for all).
|
|
4077
|
+
*/
|
|
4078
|
+
async fetchGames(year, round, complete) {
|
|
4079
|
+
const params = new URLSearchParams({ q: "games", year: String(year) });
|
|
4080
|
+
if (round != null) params.set("round", String(round));
|
|
4081
|
+
if (complete != null) params.set("complete", String(complete));
|
|
4082
|
+
const result = await this.fetchJson(params);
|
|
4083
|
+
if (!result.success) return result;
|
|
4084
|
+
const parsed = SquiggleGamesResponseSchema.safeParse(result.data);
|
|
4085
|
+
if (!parsed.success) {
|
|
4086
|
+
return err(
|
|
4087
|
+
new ScrapeError(`Invalid Squiggle games response: ${parsed.error.message}`, "squiggle")
|
|
4088
|
+
);
|
|
3598
4089
|
}
|
|
4090
|
+
return ok(parsed.data);
|
|
3599
4091
|
}
|
|
3600
|
-
|
|
3601
|
-
|
|
4092
|
+
/**
|
|
4093
|
+
* Fetch standings (ladder) from the Squiggle API.
|
|
4094
|
+
*
|
|
4095
|
+
* @param year - Season year.
|
|
4096
|
+
* @param round - Optional round number.
|
|
4097
|
+
*/
|
|
4098
|
+
async fetchStandings(year, round) {
|
|
4099
|
+
const params = new URLSearchParams({ q: "standings", year: String(year) });
|
|
4100
|
+
if (round != null) params.set("round", String(round));
|
|
4101
|
+
const result = await this.fetchJson(params);
|
|
4102
|
+
if (!result.success) return result;
|
|
4103
|
+
const parsed = SquiggleStandingsResponseSchema.safeParse(result.data);
|
|
4104
|
+
if (!parsed.success) {
|
|
3602
4105
|
return err(
|
|
3603
|
-
new ScrapeError(
|
|
3604
|
-
`Fryzigg data frame missing required column: "${group.join('" or "')}"`,
|
|
3605
|
-
"fryzigg"
|
|
3606
|
-
)
|
|
4106
|
+
new ScrapeError(`Invalid Squiggle standings response: ${parsed.error.message}`, "squiggle")
|
|
3607
4107
|
);
|
|
3608
4108
|
}
|
|
4109
|
+
return ok(parsed.data);
|
|
3609
4110
|
}
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
const cols = {
|
|
3619
|
-
matchId: getCol("match_id"),
|
|
3620
|
-
date: getCol(mapping.date),
|
|
3621
|
-
homeTeam: getCol(mapping.homeTeam),
|
|
3622
|
-
awayTeam: getCol(mapping.awayTeam),
|
|
3623
|
-
team: getCol(mapping.team),
|
|
3624
|
-
round: getCol(mapping.round),
|
|
3625
|
-
jumperNumber: getCol(mapping.jumperNumber),
|
|
3626
|
-
playerId: getCol("player_id"),
|
|
3627
|
-
firstName: getCol(mapping.firstName),
|
|
3628
|
-
lastName: getCol(mapping.lastName),
|
|
3629
|
-
playerName: getCol(mapping.playerName),
|
|
3630
|
-
kicks: getCol("kicks"),
|
|
3631
|
-
handballs: getCol("handballs"),
|
|
3632
|
-
disposals: getCol("disposals"),
|
|
3633
|
-
marks: getCol("marks"),
|
|
3634
|
-
goals: getCol("goals"),
|
|
3635
|
-
behinds: getCol("behinds"),
|
|
3636
|
-
tackles: getCol("tackles"),
|
|
3637
|
-
hitouts: getCol("hitouts"),
|
|
3638
|
-
freesFor: getCol(mapping.freesFor),
|
|
3639
|
-
freesAgainst: getCol(mapping.freesAgainst),
|
|
3640
|
-
contestedPossessions: getCol("contested_possessions"),
|
|
3641
|
-
uncontestedPossessions: getCol("uncontested_possessions"),
|
|
3642
|
-
contestedMarks: getCol("contested_marks"),
|
|
3643
|
-
intercepts: getCol("intercepts"),
|
|
3644
|
-
centreClearances: getCol("centre_clearances"),
|
|
3645
|
-
stoppageClearances: getCol("stoppage_clearances"),
|
|
3646
|
-
totalClearances: getCol(mapping.totalClearances),
|
|
3647
|
-
inside50s: getCol(mapping.inside50s),
|
|
3648
|
-
rebound50s: getCol(mapping.rebound50s),
|
|
3649
|
-
clangers: getCol("clangers"),
|
|
3650
|
-
turnovers: getCol("turnovers"),
|
|
3651
|
-
onePercenters: getCol("one_percenters"),
|
|
3652
|
-
bounces: getCol("bounces"),
|
|
3653
|
-
goalAssists: getCol("goal_assists"),
|
|
3654
|
-
disposalEfficiency: getCol(mapping.disposalEfficiency),
|
|
3655
|
-
metresGained: getCol("metres_gained"),
|
|
3656
|
-
marksInside50: getCol(mapping.marksInside50),
|
|
3657
|
-
tacklesInside50: getCol(mapping.tacklesInside50),
|
|
3658
|
-
shotsAtGoal: getCol("shots_at_goal"),
|
|
3659
|
-
scoreInvolvements: getCol("score_involvements"),
|
|
3660
|
-
totalPossessions: getCol(mapping.totalPossessions),
|
|
3661
|
-
timeOnGround: getCol(mapping.timeOnGround),
|
|
3662
|
-
ratingPoints: getCol("rating_points"),
|
|
3663
|
-
position: getCol(mapping.position),
|
|
3664
|
-
brownlowVotes: getCol("brownlow_votes"),
|
|
3665
|
-
supercoachScore: getCol("supercoach_score"),
|
|
3666
|
-
dreamTeamPoints: getCol(mapping.dreamTeamPoints),
|
|
3667
|
-
effectiveDisposals: getCol("effective_disposals"),
|
|
3668
|
-
effectiveKicks: getCol("effective_kicks"),
|
|
3669
|
-
pressureActs: getCol("pressure_acts"),
|
|
3670
|
-
defHalfPressureActs: getCol("def_half_pressure_acts"),
|
|
3671
|
-
spoils: getCol("spoils"),
|
|
3672
|
-
hitoutsToAdvantage: getCol("hitouts_to_advantage"),
|
|
3673
|
-
hitoutWinPercentage: getCol("hitout_win_percentage"),
|
|
3674
|
-
groundBallGets: getCol("ground_ball_gets"),
|
|
3675
|
-
f50GroundBallGets: getCol("f50_ground_ball_gets"),
|
|
3676
|
-
interceptMarks: getCol("intercept_marks"),
|
|
3677
|
-
marksOnLead: getCol("marks_on_lead"),
|
|
3678
|
-
contestOffOneOnOnes: getCol("contest_off_one_on_ones"),
|
|
3679
|
-
contestOffWins: getCol("contest_off_wins"),
|
|
3680
|
-
contestDefOneOnOnes: getCol("contest_def_one_on_ones"),
|
|
3681
|
-
contestDefLosses: getCol("contest_def_losses"),
|
|
3682
|
-
ruckContests: getCol("ruck_contests"),
|
|
3683
|
-
scoreLaunches: getCol("score_launches")
|
|
3684
|
-
};
|
|
3685
|
-
const dateCol = cols.date;
|
|
3686
|
-
const roundCol = cols.round;
|
|
3687
|
-
const nRows = dateCol ? dateCol.length : 0;
|
|
3688
|
-
const hasFilters = options.season !== void 0 || options.round !== void 0;
|
|
3689
|
-
let rowIndices = null;
|
|
3690
|
-
let rowCount = nRows;
|
|
3691
|
-
if (hasFilters) {
|
|
3692
|
-
const matching = [];
|
|
3693
|
-
for (let i = 0; i < nRows; i++) {
|
|
3694
|
-
if (options.season !== void 0) {
|
|
3695
|
-
const dateStr = dateCol?.[i];
|
|
3696
|
-
if (typeof dateStr !== "string") continue;
|
|
3697
|
-
const year = Number(dateStr.slice(0, 4));
|
|
3698
|
-
if (year !== options.season) continue;
|
|
3699
|
-
}
|
|
3700
|
-
if (options.round !== void 0 && roundCol) {
|
|
3701
|
-
const roundVal = roundCol[i];
|
|
3702
|
-
const roundNum = typeof roundVal === "string" ? Number(roundVal) : roundVal;
|
|
3703
|
-
if (roundNum !== options.round) continue;
|
|
3704
|
-
}
|
|
3705
|
-
matching.push(i);
|
|
3706
|
-
}
|
|
3707
|
-
rowIndices = matching;
|
|
3708
|
-
rowCount = matching.length;
|
|
4111
|
+
};
|
|
4112
|
+
|
|
4113
|
+
// src/sources/adapters/squiggle.ts
|
|
4114
|
+
var SQUIGGLE_MATCH_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 2012 }]]);
|
|
4115
|
+
var SQUIGGLE_LADDER_COVERAGE = /* @__PURE__ */ new Map([["AFLM", { minSeason: 2012 }]]);
|
|
4116
|
+
var SquiggleMatchSource = class {
|
|
4117
|
+
constructor(client = new SquiggleClient()) {
|
|
4118
|
+
this.client = client;
|
|
3709
4119
|
}
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
4120
|
+
id = "squiggle";
|
|
4121
|
+
coverage = SQUIGGLE_MATCH_COVERAGE;
|
|
4122
|
+
async fetchMatches(query) {
|
|
4123
|
+
const result = await this.client.fetchGames(query.season, query.round ?? void 0, 100);
|
|
4124
|
+
if (!result.success) return result;
|
|
4125
|
+
return ok(transformSquiggleGamesToFixture(result.data.games, query.season));
|
|
3714
4126
|
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
const v = column[i];
|
|
3720
|
-
return typeof v === "number" ? v : null;
|
|
3721
|
-
}
|
|
3722
|
-
function strAt(column, i) {
|
|
3723
|
-
if (!column) return null;
|
|
3724
|
-
const v = column[i];
|
|
3725
|
-
return typeof v === "string" ? v : null;
|
|
3726
|
-
}
|
|
3727
|
-
function roundAt(column, i) {
|
|
3728
|
-
if (!column) return 0;
|
|
3729
|
-
const v = column[i];
|
|
3730
|
-
if (typeof v === "number") return v;
|
|
3731
|
-
if (typeof v === "string") {
|
|
3732
|
-
const n = Number(v);
|
|
3733
|
-
return Number.isNaN(n) ? 0 : n;
|
|
4127
|
+
};
|
|
4128
|
+
var SquiggleLadderSource = class {
|
|
4129
|
+
constructor(client = new SquiggleClient()) {
|
|
4130
|
+
this.client = client;
|
|
3734
4131
|
}
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
firstName = strAt(c.firstName, i) ?? "";
|
|
3748
|
-
lastName = strAt(c.lastName, i) ?? "";
|
|
4132
|
+
id = "squiggle";
|
|
4133
|
+
coverage = SQUIGGLE_LADDER_COVERAGE;
|
|
4134
|
+
async fetchLadder(query) {
|
|
4135
|
+
const competition = query.competition ?? "AFLM";
|
|
4136
|
+
const result = await this.client.fetchStandings(query.season, query.round ?? void 0);
|
|
4137
|
+
if (!result.success) return result;
|
|
4138
|
+
return ok({
|
|
4139
|
+
season: query.season,
|
|
4140
|
+
roundNumber: query.round ?? null,
|
|
4141
|
+
entries: transformSquiggleStandings(result.data.standings),
|
|
4142
|
+
competition
|
|
4143
|
+
});
|
|
3749
4144
|
}
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
onePercenters: numAt(c.onePercenters, i),
|
|
3789
|
-
bounces: numAt(c.bounces, i),
|
|
3790
|
-
goalAssists: numAt(c.goalAssists, i),
|
|
3791
|
-
disposalEfficiency: numAt(c.disposalEfficiency, i),
|
|
3792
|
-
metresGained: numAt(c.metresGained, i),
|
|
3793
|
-
goalAccuracy: null,
|
|
3794
|
-
marksInside50: numAt(c.marksInside50, i),
|
|
3795
|
-
tacklesInside50: numAt(c.tacklesInside50, i),
|
|
3796
|
-
shotsAtGoal: numAt(c.shotsAtGoal, i),
|
|
3797
|
-
scoreInvolvements: numAt(c.scoreInvolvements, i),
|
|
3798
|
-
totalPossessions: numAt(c.totalPossessions, i),
|
|
3799
|
-
timeOnGroundPercentage: numAt(c.timeOnGround, i),
|
|
3800
|
-
ratingPoints: numAt(c.ratingPoints, i),
|
|
3801
|
-
position: strAt(c.position, i),
|
|
3802
|
-
goalEfficiency: null,
|
|
3803
|
-
shotEfficiency: null,
|
|
3804
|
-
interchangeCounts: null,
|
|
3805
|
-
brownlowVotes: numAt(c.brownlowVotes, i),
|
|
3806
|
-
supercoachScore: numAt(c.supercoachScore, i),
|
|
3807
|
-
dreamTeamPoints: numAt(c.dreamTeamPoints, i),
|
|
3808
|
-
effectiveDisposals: numAt(c.effectiveDisposals, i),
|
|
3809
|
-
effectiveKicks: numAt(c.effectiveKicks, i),
|
|
3810
|
-
kickEfficiency: null,
|
|
3811
|
-
kickToHandballRatio: null,
|
|
3812
|
-
pressureActs: numAt(c.pressureActs, i),
|
|
3813
|
-
defHalfPressureActs: numAt(c.defHalfPressureActs, i),
|
|
3814
|
-
spoils: numAt(c.spoils, i),
|
|
3815
|
-
hitoutsToAdvantage: numAt(c.hitoutsToAdvantage, i),
|
|
3816
|
-
hitoutWinPercentage: numAt(c.hitoutWinPercentage, i),
|
|
3817
|
-
hitoutToAdvantageRate: null,
|
|
3818
|
-
groundBallGets: numAt(c.groundBallGets, i),
|
|
3819
|
-
f50GroundBallGets: numAt(c.f50GroundBallGets, i),
|
|
3820
|
-
interceptMarks: numAt(c.interceptMarks, i),
|
|
3821
|
-
marksOnLead: numAt(c.marksOnLead, i),
|
|
3822
|
-
contestedPossessionRate: null,
|
|
3823
|
-
contestOffOneOnOnes: numAt(c.contestOffOneOnOnes, i),
|
|
3824
|
-
contestOffWins: numAt(c.contestOffWins, i),
|
|
3825
|
-
contestOffWinsPercentage: null,
|
|
3826
|
-
contestDefOneOnOnes: numAt(c.contestDefOneOnOnes, i),
|
|
3827
|
-
contestDefLosses: numAt(c.contestDefLosses, i),
|
|
3828
|
-
contestDefLossPercentage: null,
|
|
3829
|
-
centreBounceAttendances: null,
|
|
3830
|
-
kickins: null,
|
|
3831
|
-
kickinsPlayon: null,
|
|
3832
|
-
ruckContests: numAt(c.ruckContests, i),
|
|
3833
|
-
scoreLaunches: numAt(c.scoreLaunches, i),
|
|
3834
|
-
source: "fryzigg"
|
|
3835
|
-
};
|
|
4145
|
+
};
|
|
4146
|
+
|
|
4147
|
+
// src/sources/adapters/coverage.ts
|
|
4148
|
+
function checkCoverage(coverage, request, suggestion) {
|
|
4149
|
+
const range = coverage.get(request.competition);
|
|
4150
|
+
if (!range) {
|
|
4151
|
+
return err(
|
|
4152
|
+
new UnsupportedCompetitionError(
|
|
4153
|
+
`${request.source} does not provide ${request.operation} data for ${request.competition}`,
|
|
4154
|
+
request.source,
|
|
4155
|
+
request.competition,
|
|
4156
|
+
suggestion
|
|
4157
|
+
)
|
|
4158
|
+
);
|
|
4159
|
+
}
|
|
4160
|
+
if (request.season < range.minSeason) {
|
|
4161
|
+
return err(
|
|
4162
|
+
new OutOfRangeError(
|
|
4163
|
+
`${request.source} only covers ${request.competition} ${request.operation} from ${range.minSeason}; you asked for ${request.season}`,
|
|
4164
|
+
request.source,
|
|
4165
|
+
request.competition,
|
|
4166
|
+
request.season,
|
|
4167
|
+
suggestion
|
|
4168
|
+
)
|
|
4169
|
+
);
|
|
4170
|
+
}
|
|
4171
|
+
if (range.maxSeason != null && request.season > range.maxSeason) {
|
|
4172
|
+
return err(
|
|
4173
|
+
new OutOfRangeError(
|
|
4174
|
+
`${request.source} only covers ${request.competition} ${request.operation} up to ${range.maxSeason}; you asked for ${request.season}`,
|
|
4175
|
+
request.source,
|
|
4176
|
+
request.competition,
|
|
4177
|
+
request.season,
|
|
4178
|
+
suggestion
|
|
4179
|
+
)
|
|
4180
|
+
);
|
|
4181
|
+
}
|
|
4182
|
+
return ok(void 0);
|
|
3836
4183
|
}
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
4184
|
+
function unsupportedSourceForOperation(source, operation, registered) {
|
|
4185
|
+
return new UnsupportedSourceError(
|
|
4186
|
+
`${source} does not provide ${operation} data. Supported sources: ${registered.join(", ")}.`,
|
|
4187
|
+
source
|
|
4188
|
+
);
|
|
3841
4189
|
}
|
|
3842
|
-
function
|
|
3843
|
-
const
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
season
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
),
|
|
3853
|
-
competition: ctx.competition,
|
|
3854
|
-
date: ctx.date ?? null,
|
|
3855
|
-
homeTeam: ctx.homeTeam ?? null,
|
|
3856
|
-
awayTeam: ctx.awayTeam ?? null,
|
|
3857
|
-
playerId: inner.playerId,
|
|
3858
|
-
givenName: inner.playerName.givenName,
|
|
3859
|
-
surname: inner.playerName.surname,
|
|
3860
|
-
displayName: `${inner.playerName.givenName} ${inner.playerName.surname}`,
|
|
3861
|
-
jumperNumber: item.player.jumperNumber ?? null,
|
|
3862
|
-
kicks: toNullable(stats?.kicks),
|
|
3863
|
-
handballs: toNullable(stats?.handballs),
|
|
3864
|
-
disposals: toNullable(stats?.disposals),
|
|
3865
|
-
marks: toNullable(stats?.marks),
|
|
3866
|
-
goals: toNullable(stats?.goals),
|
|
3867
|
-
behinds: toNullable(stats?.behinds),
|
|
3868
|
-
tackles: toNullable(stats?.tackles),
|
|
3869
|
-
hitouts: toNullable(stats?.hitouts),
|
|
3870
|
-
freesFor: toNullable(stats?.freesFor),
|
|
3871
|
-
freesAgainst: toNullable(stats?.freesAgainst),
|
|
3872
|
-
contestedPossessions: toNullable(stats?.contestedPossessions),
|
|
3873
|
-
uncontestedPossessions: toNullable(stats?.uncontestedPossessions),
|
|
3874
|
-
contestedMarks: toNullable(stats?.contestedMarks),
|
|
3875
|
-
intercepts: toNullable(stats?.intercepts),
|
|
3876
|
-
centreClearances: toNullable(clearances?.centreClearances),
|
|
3877
|
-
stoppageClearances: toNullable(clearances?.stoppageClearances),
|
|
3878
|
-
totalClearances: toNullable(clearances?.totalClearances),
|
|
3879
|
-
inside50s: toNullable(stats?.inside50s),
|
|
3880
|
-
rebound50s: toNullable(stats?.rebound50s),
|
|
3881
|
-
clangers: toNullable(stats?.clangers),
|
|
3882
|
-
turnovers: toNullable(stats?.turnovers),
|
|
3883
|
-
onePercenters: toNullable(stats?.onePercenters),
|
|
3884
|
-
bounces: toNullable(stats?.bounces),
|
|
3885
|
-
goalAssists: toNullable(stats?.goalAssists),
|
|
3886
|
-
disposalEfficiency: toNullable(stats?.disposalEfficiency),
|
|
3887
|
-
metresGained: toNullable(stats?.metresGained),
|
|
3888
|
-
goalAccuracy: toNullable(stats?.goalAccuracy),
|
|
3889
|
-
marksInside50: toNullable(stats?.marksInside50),
|
|
3890
|
-
tacklesInside50: toNullable(stats?.tacklesInside50),
|
|
3891
|
-
shotsAtGoal: toNullable(stats?.shotsAtGoal),
|
|
3892
|
-
scoreInvolvements: toNullable(stats?.scoreInvolvements),
|
|
3893
|
-
totalPossessions: toNullable(stats?.totalPossessions),
|
|
3894
|
-
timeOnGroundPercentage: toNullable(item.playerStats?.timeOnGroundPercentage),
|
|
3895
|
-
ratingPoints: toNullable(stats?.ratingPoints),
|
|
3896
|
-
position: item.player.player.position ?? null,
|
|
3897
|
-
goalEfficiency: toNullable(stats?.goalEfficiency),
|
|
3898
|
-
shotEfficiency: toNullable(stats?.shotEfficiency),
|
|
3899
|
-
interchangeCounts: toNullable(stats?.interchangeCounts),
|
|
3900
|
-
brownlowVotes: toNullable(stats?.brownlowVotes),
|
|
3901
|
-
supercoachScore: null,
|
|
3902
|
-
dreamTeamPoints: toNullable(stats?.dreamTeamPoints),
|
|
3903
|
-
effectiveDisposals: toNullable(stats?.extendedStats?.effectiveDisposals),
|
|
3904
|
-
effectiveKicks: toNullable(stats?.extendedStats?.effectiveKicks),
|
|
3905
|
-
kickEfficiency: toNullable(stats?.extendedStats?.kickEfficiency),
|
|
3906
|
-
kickToHandballRatio: toNullable(stats?.extendedStats?.kickToHandballRatio),
|
|
3907
|
-
pressureActs: toNullable(stats?.extendedStats?.pressureActs),
|
|
3908
|
-
defHalfPressureActs: toNullable(stats?.extendedStats?.defHalfPressureActs),
|
|
3909
|
-
spoils: toNullable(stats?.extendedStats?.spoils),
|
|
3910
|
-
hitoutsToAdvantage: toNullable(stats?.extendedStats?.hitoutsToAdvantage),
|
|
3911
|
-
hitoutWinPercentage: toNullable(stats?.extendedStats?.hitoutWinPercentage),
|
|
3912
|
-
hitoutToAdvantageRate: toNullable(stats?.extendedStats?.hitoutToAdvantageRate),
|
|
3913
|
-
groundBallGets: toNullable(stats?.extendedStats?.groundBallGets),
|
|
3914
|
-
f50GroundBallGets: toNullable(stats?.extendedStats?.f50GroundBallGets),
|
|
3915
|
-
interceptMarks: toNullable(stats?.extendedStats?.interceptMarks),
|
|
3916
|
-
marksOnLead: toNullable(stats?.extendedStats?.marksOnLead),
|
|
3917
|
-
contestedPossessionRate: toNullable(stats?.extendedStats?.contestedPossessionRate),
|
|
3918
|
-
contestOffOneOnOnes: toNullable(stats?.extendedStats?.contestOffOneOnOnes),
|
|
3919
|
-
contestOffWins: toNullable(stats?.extendedStats?.contestOffWins),
|
|
3920
|
-
contestOffWinsPercentage: toNullable(stats?.extendedStats?.contestOffWinsPercentage),
|
|
3921
|
-
contestDefOneOnOnes: toNullable(stats?.extendedStats?.contestDefOneOnOnes),
|
|
3922
|
-
contestDefLosses: toNullable(stats?.extendedStats?.contestDefLosses),
|
|
3923
|
-
contestDefLossPercentage: toNullable(stats?.extendedStats?.contestDefLossPercentage),
|
|
3924
|
-
centreBounceAttendances: toNullable(stats?.extendedStats?.centreBounceAttendances),
|
|
3925
|
-
kickins: toNullable(stats?.extendedStats?.kickins),
|
|
3926
|
-
kickinsPlayon: toNullable(stats?.extendedStats?.kickinsPlayon),
|
|
3927
|
-
ruckContests: toNullable(stats?.extendedStats?.ruckContests),
|
|
3928
|
-
scoreLaunches: toNullable(stats?.extendedStats?.scoreLaunches),
|
|
3929
|
-
source: ctx.source
|
|
3930
|
-
};
|
|
4190
|
+
function findAlternativeSource(adapters, request) {
|
|
4191
|
+
for (const adapter of adapters) {
|
|
4192
|
+
if (adapter.id === request.source) continue;
|
|
4193
|
+
const range = adapter.coverage.get(request.competition);
|
|
4194
|
+
if (!range) continue;
|
|
4195
|
+
if (request.season < range.minSeason) continue;
|
|
4196
|
+
if (range.maxSeason != null && request.season > range.maxSeason) continue;
|
|
4197
|
+
return adapter.id;
|
|
4198
|
+
}
|
|
4199
|
+
return void 0;
|
|
3931
4200
|
}
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
4201
|
+
|
|
4202
|
+
// src/sources/adapters/dispatch.ts
|
|
4203
|
+
function dispatch(registry, operation, query) {
|
|
4204
|
+
const adapter = registry.get(query.source);
|
|
4205
|
+
if (!adapter) {
|
|
4206
|
+
return err(unsupportedSourceForOperation(query.source, operation, registry.list()));
|
|
4207
|
+
}
|
|
4208
|
+
const competition = query.competition ?? "AFLM";
|
|
4209
|
+
const alternative = findAlternativeSource(registry.all(), {
|
|
4210
|
+
source: query.source,
|
|
4211
|
+
competition,
|
|
4212
|
+
season: query.season
|
|
4213
|
+
});
|
|
4214
|
+
const suggestion = alternative ? `--source ${alternative}` : void 0;
|
|
4215
|
+
const coverage = checkCoverage(
|
|
4216
|
+
adapter.coverage,
|
|
4217
|
+
{ source: query.source, operation, competition, season: query.season },
|
|
4218
|
+
suggestion
|
|
4219
|
+
);
|
|
4220
|
+
if (!coverage.success) return coverage;
|
|
4221
|
+
return ok(adapter);
|
|
3936
4222
|
}
|
|
3937
4223
|
|
|
4224
|
+
// src/sources/adapters/index.ts
|
|
4225
|
+
matchRegistry.register(new AflApiMatchSource());
|
|
4226
|
+
matchRegistry.register(new FootyWireMatchSource());
|
|
4227
|
+
matchRegistry.register(new AflTablesMatchSource());
|
|
4228
|
+
matchRegistry.register(new SquiggleMatchSource());
|
|
4229
|
+
playerStatsRegistry.register(new AflApiPlayerStatsSource());
|
|
4230
|
+
playerStatsRegistry.register(new FootyWirePlayerStatsSource());
|
|
4231
|
+
playerStatsRegistry.register(new AflTablesPlayerStatsSource());
|
|
4232
|
+
playerStatsRegistry.register(new FryziggPlayerStatsSource());
|
|
4233
|
+
teamStatsRegistry.register(new FootyWireTeamStatsSource());
|
|
4234
|
+
teamStatsRegistry.register(new AflTablesTeamStatsSource());
|
|
4235
|
+
squadRegistry.register(new AflApiSquadSource());
|
|
4236
|
+
squadRegistry.register(new FootyWireSquadSource());
|
|
4237
|
+
squadRegistry.register(new AflTablesSquadSource());
|
|
4238
|
+
lineupRegistry.register(new AflApiLineupSource());
|
|
4239
|
+
ladderRegistry.register(new AflApiLadderSource());
|
|
4240
|
+
ladderRegistry.register(new AflTablesLadderSource());
|
|
4241
|
+
ladderRegistry.register(new SquiggleLadderSource());
|
|
4242
|
+
|
|
3938
4243
|
// src/api/player-stats.ts
|
|
3939
4244
|
async function fetchPlayerStats(query) {
|
|
3940
|
-
const
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
teamIdMap2.set(match.awayTeamId, normaliseTeamName(match.awayTeam.name));
|
|
3955
|
-
}
|
|
3956
|
-
return ok(
|
|
3957
|
-
transformPlayerStats(statsResult.data, {
|
|
3958
|
-
matchId: query.matchId,
|
|
3959
|
-
season: query.season,
|
|
3960
|
-
roundNumber: query.round ?? 0,
|
|
3961
|
-
competition,
|
|
3962
|
-
source: "afl-api",
|
|
3963
|
-
teamIdMap: teamIdMap2
|
|
3964
|
-
})
|
|
3965
|
-
);
|
|
3966
|
-
}
|
|
3967
|
-
const seasonResult = await client.resolveCompSeason(competition, query.season);
|
|
3968
|
-
if (!seasonResult.success) return seasonResult;
|
|
3969
|
-
const matchItemsResult = query.round != null ? await client.fetchRoundMatchItemsByNumber(seasonResult.data, query.round) : await client.fetchSeasonMatchItems(seasonResult.data);
|
|
3970
|
-
if (!matchItemsResult.success) return matchItemsResult;
|
|
3971
|
-
const teamIdMap = /* @__PURE__ */ new Map();
|
|
3972
|
-
for (const item of matchItemsResult.data) {
|
|
3973
|
-
teamIdMap.set(item.match.homeTeamId, item.match.homeTeam.name);
|
|
3974
|
-
teamIdMap.set(item.match.awayTeamId, item.match.awayTeam.name);
|
|
3975
|
-
}
|
|
3976
|
-
const statsResults = await batchedMap(
|
|
3977
|
-
matchItemsResult.data,
|
|
3978
|
-
(item) => client.fetchPlayerStats(item.match.matchId)
|
|
4245
|
+
const adapterR = dispatch(playerStatsRegistry, "player stats", query);
|
|
4246
|
+
return Result.flatMapAsync(adapterR, (a) => a.fetchPlayerStats(query));
|
|
4247
|
+
}
|
|
4248
|
+
|
|
4249
|
+
// src/api/awards.ts
|
|
4250
|
+
var FOOTYWIRE_BASE2 = "https://www.footywire.com/afl/footy";
|
|
4251
|
+
async function fetchAwards(query) {
|
|
4252
|
+
switch (query.award) {
|
|
4253
|
+
case "brownlow":
|
|
4254
|
+
return fetchFootyWireAward(
|
|
4255
|
+
`${FOOTYWIRE_BASE2}/brownlow_medal?year=${query.season}`,
|
|
4256
|
+
(html) => parseBrownlowVotes(html, query.season),
|
|
4257
|
+
"Brownlow",
|
|
4258
|
+
query.season
|
|
3979
4259
|
);
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4260
|
+
case "all-australian":
|
|
4261
|
+
return fetchFootyWireAward(
|
|
4262
|
+
`${FOOTYWIRE_BASE2}/all_australian_selection?year=${query.season}`,
|
|
4263
|
+
(html) => parseAllAustralian(html, query.season),
|
|
4264
|
+
"All-Australian",
|
|
4265
|
+
query.season
|
|
4266
|
+
);
|
|
4267
|
+
case "rising-star":
|
|
4268
|
+
return fetchFootyWireAward(
|
|
4269
|
+
`${FOOTYWIRE_BASE2}/rising_star_nominations?year=${query.season}`,
|
|
4270
|
+
(html) => parseRisingStarNominations(html, query.season),
|
|
4271
|
+
"Rising Star",
|
|
4272
|
+
query.season
|
|
4273
|
+
);
|
|
4274
|
+
case "coaches":
|
|
4275
|
+
return fetchCoachesVotes(query);
|
|
4276
|
+
case "coleman":
|
|
4277
|
+
return fetchColemanLeaderboard(query);
|
|
4278
|
+
default:
|
|
4279
|
+
return err(
|
|
4280
|
+
new ScrapeError(`Unknown award type: ${query.award}`, "footywire")
|
|
4281
|
+
);
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
async function fetchFootyWireAward(url, parse, label, season) {
|
|
4285
|
+
const client = new FootyWireClient();
|
|
4286
|
+
const htmlResult = await client.fetchPage(url);
|
|
4287
|
+
if (!htmlResult.success) return htmlResult;
|
|
4288
|
+
const data = parse(htmlResult.data);
|
|
4289
|
+
if (data.length === 0) {
|
|
4290
|
+
return err(new ScrapeError(`No ${label} data found for season ${season}`, "footywire"));
|
|
4291
|
+
}
|
|
4292
|
+
return ok(data);
|
|
4293
|
+
}
|
|
4294
|
+
async function fetchCoachesVotes(query) {
|
|
4295
|
+
const competition = query.competition ?? "AFLM";
|
|
4296
|
+
if (query.season < 2006) {
|
|
4297
|
+
return err(new ScrapeError("No coaches votes data available before 2006", "afl-coaches"));
|
|
4298
|
+
}
|
|
4299
|
+
if (competition === "AFLW" && query.season < 2018) {
|
|
4300
|
+
return err(new ScrapeError("No AFLW coaches votes data available before 2018", "afl-coaches"));
|
|
4301
|
+
}
|
|
4302
|
+
if (competition === "VFL" || competition === "VFLW") {
|
|
4303
|
+
return err(
|
|
4304
|
+
new ScrapeError(`No coaches votes data available for ${competition}`, "afl-coaches")
|
|
4305
|
+
);
|
|
4306
|
+
}
|
|
4307
|
+
const client = new AflCoachesClient();
|
|
4308
|
+
const result = query.round != null ? await client.scrapeRoundVotes(
|
|
4309
|
+
query.season,
|
|
4310
|
+
query.round,
|
|
4311
|
+
competition,
|
|
4312
|
+
query.round >= 24 && query.season >= 2018
|
|
4313
|
+
) : await client.fetchSeasonVotes(query.season, competition);
|
|
4314
|
+
if (!result.success) return result;
|
|
4315
|
+
let votes = result.data;
|
|
4316
|
+
if (query.team != null) {
|
|
4317
|
+
const target = normaliseTeamName(query.team);
|
|
4318
|
+
votes = votes.filter(
|
|
4319
|
+
(v) => normaliseTeamName(v.homeTeam) === target || normaliseTeamName(v.awayTeam) === target
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
return ok(votes);
|
|
4323
|
+
}
|
|
4324
|
+
async function fetchColemanLeaderboard(query) {
|
|
4325
|
+
const competition = query.competition ?? "AFLM";
|
|
4326
|
+
if (competition === "VFL" || competition === "VFLW") {
|
|
4327
|
+
return err(
|
|
4328
|
+
new ScrapeError(
|
|
4329
|
+
`Coleman Medal is not awarded in ${competition}; use Coleman-equivalent stats query`,
|
|
4330
|
+
"afl-api"
|
|
4331
|
+
)
|
|
4332
|
+
);
|
|
4333
|
+
}
|
|
4334
|
+
const statsR = await fetchPlayerStats({
|
|
4335
|
+
source: "afl-api",
|
|
4336
|
+
season: query.season,
|
|
4337
|
+
competition
|
|
4338
|
+
});
|
|
4339
|
+
return Result.map(statsR, (stats) => rankColemanFromStats(stats, query.season, query.limit));
|
|
4340
|
+
}
|
|
4341
|
+
function rankColemanFromStats(stats, season, limit) {
|
|
4342
|
+
const accumulator = /* @__PURE__ */ new Map();
|
|
4343
|
+
for (const s of stats) {
|
|
4344
|
+
if (s.goals == null) continue;
|
|
4345
|
+
const key = s.playerId;
|
|
4346
|
+
const existing = accumulator.get(key);
|
|
4347
|
+
if (existing) {
|
|
4348
|
+
existing.goals += s.goals;
|
|
4349
|
+
existing.gamesPlayed += 1;
|
|
4350
|
+
} else {
|
|
4351
|
+
accumulator.set(key, {
|
|
4352
|
+
player: s.displayName,
|
|
4353
|
+
team: s.team,
|
|
4354
|
+
goals: s.goals,
|
|
4355
|
+
gamesPlayed: 1
|
|
4048
4356
|
});
|
|
4049
4357
|
}
|
|
4050
|
-
default:
|
|
4051
|
-
return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
|
|
4052
4358
|
}
|
|
4359
|
+
const ranked = [...accumulator.values()].filter((entry) => entry.goals > 0).sort((a, b) => b.goals - a.goals);
|
|
4360
|
+
let lastGoals = -1;
|
|
4361
|
+
let lastPosition = 0;
|
|
4362
|
+
const leaderboard = ranked.map((entry, index) => {
|
|
4363
|
+
const position = entry.goals === lastGoals ? lastPosition : index + 1;
|
|
4364
|
+
lastGoals = entry.goals;
|
|
4365
|
+
lastPosition = position;
|
|
4366
|
+
return {
|
|
4367
|
+
type: "coleman",
|
|
4368
|
+
season,
|
|
4369
|
+
position,
|
|
4370
|
+
player: entry.player,
|
|
4371
|
+
team: entry.team,
|
|
4372
|
+
goals: entry.goals,
|
|
4373
|
+
gamesPlayed: entry.gamesPlayed
|
|
4374
|
+
};
|
|
4375
|
+
});
|
|
4376
|
+
return limit != null ? leaderboard.slice(0, limit) : leaderboard;
|
|
4053
4377
|
}
|
|
4054
4378
|
|
|
4055
|
-
// src/api/
|
|
4056
|
-
async function
|
|
4057
|
-
const
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
gpMap.set(away, (gpMap.get(away) ?? 0) + 1);
|
|
4077
|
-
}
|
|
4078
|
-
}
|
|
4079
|
-
}
|
|
4080
|
-
const enriched = statsResult.data.map((entry) => ({
|
|
4081
|
-
...entry,
|
|
4082
|
-
gamesPlayed: gpMap.get(normaliseTeamName(entry.team)) ?? entry.gamesPlayed
|
|
4083
|
-
}));
|
|
4084
|
-
if (summaryType === "averages") {
|
|
4085
|
-
return ok(
|
|
4086
|
-
enriched.map((entry) => ({
|
|
4087
|
-
...entry,
|
|
4088
|
-
stats: Object.fromEntries(
|
|
4089
|
-
Object.entries(entry.stats).map(([k, v]) => [
|
|
4090
|
-
k,
|
|
4091
|
-
entry.gamesPlayed > 0 ? +(v / entry.gamesPlayed).toFixed(1) : 0
|
|
4092
|
-
])
|
|
4093
|
-
)
|
|
4094
|
-
}))
|
|
4095
|
-
);
|
|
4096
|
-
}
|
|
4097
|
-
return ok(enriched);
|
|
4098
|
-
}
|
|
4099
|
-
case "afl-api":
|
|
4100
|
-
case "squiggle":
|
|
4101
|
-
return err(
|
|
4102
|
-
new UnsupportedSourceError(
|
|
4103
|
-
`Team stats are not available from ${query.source}. Use "footywire" or "afl-tables".`,
|
|
4104
|
-
query.source
|
|
4105
|
-
)
|
|
4106
|
-
);
|
|
4107
|
-
default:
|
|
4108
|
-
return err(new UnsupportedSourceError(`Unsupported source: ${query.source}`, query.source));
|
|
4379
|
+
// src/api/ladder.ts
|
|
4380
|
+
async function fetchLadder(query) {
|
|
4381
|
+
const adapterR = dispatch(ladderRegistry, "ladder", query);
|
|
4382
|
+
return Result.flatMapAsync(adapterR, (a) => a.fetchLadder(query));
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
// src/api/lineup.ts
|
|
4386
|
+
async function fetchLineup(query) {
|
|
4387
|
+
const adapterR = dispatch(lineupRegistry, "lineup", query);
|
|
4388
|
+
return Result.flatMapAsync(adapterR, (a) => a.fetchLineup(query));
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
// src/transforms/match.ts
|
|
4392
|
+
function filterMatches(matches, query) {
|
|
4393
|
+
let filtered = matches;
|
|
4394
|
+
if (query.matchId !== void 0) {
|
|
4395
|
+
filtered = filtered.filter((m) => m.matchId === query.matchId);
|
|
4396
|
+
}
|
|
4397
|
+
if (query.team !== void 0) {
|
|
4398
|
+
const target = normaliseTeamName(query.team);
|
|
4399
|
+
filtered = filtered.filter((m) => m.homeTeam === target || m.awayTeam === target);
|
|
4109
4400
|
}
|
|
4401
|
+
if (query.status !== void 0) {
|
|
4402
|
+
filtered = filtered.filter((m) => m.status === query.status);
|
|
4403
|
+
}
|
|
4404
|
+
return [...filtered];
|
|
4110
4405
|
}
|
|
4111
4406
|
|
|
4112
|
-
// src/api/
|
|
4113
|
-
function
|
|
4114
|
-
|
|
4407
|
+
// src/api/match.ts
|
|
4408
|
+
async function fetchMatches(query) {
|
|
4409
|
+
const adapterR = dispatch(matchRegistry, "match", query);
|
|
4410
|
+
const fetchedR = await Result.flatMapAsync(adapterR, (a) => a.fetchMatches(query));
|
|
4411
|
+
return Result.map(fetchedR, (matches) => filterMatches(matches, query));
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
// src/transforms/player-details.ts
|
|
4415
|
+
function squadToPlayerDetails(squad, source) {
|
|
4416
|
+
return squad.players.map((p) => ({
|
|
4417
|
+
playerId: p.playerId,
|
|
4418
|
+
givenName: p.givenName,
|
|
4419
|
+
surname: p.surname,
|
|
4420
|
+
displayName: p.displayName,
|
|
4421
|
+
team: squad.teamName,
|
|
4422
|
+
jumperNumber: p.jumperNumber,
|
|
4423
|
+
position: p.position,
|
|
4424
|
+
dateOfBirth: p.dateOfBirth ? p.dateOfBirth.toISOString().slice(0, 10) : null,
|
|
4425
|
+
heightCm: p.heightCm,
|
|
4426
|
+
weightKg: p.weightKg,
|
|
4427
|
+
gamesPlayed: p.gamesPlayed ?? null,
|
|
4428
|
+
goals: p.goals ?? null,
|
|
4429
|
+
draftYear: p.draftYear,
|
|
4430
|
+
draftPosition: p.draftPosition,
|
|
4431
|
+
draftType: p.draftType,
|
|
4432
|
+
debutYear: p.debutYear,
|
|
4433
|
+
recruitedFrom: p.recruitedFrom,
|
|
4434
|
+
source,
|
|
4435
|
+
competition: squad.competition
|
|
4436
|
+
}));
|
|
4115
4437
|
}
|
|
4438
|
+
|
|
4439
|
+
// src/api/teams.ts
|
|
4116
4440
|
function toTeams(data, competition) {
|
|
4117
4441
|
return data.map((t) => ({
|
|
4118
4442
|
teamId: String(t.id),
|
|
@@ -4123,55 +4447,65 @@ function toTeams(data, competition) {
|
|
|
4123
4447
|
}
|
|
4124
4448
|
async function fetchTeams(query) {
|
|
4125
4449
|
const client = new AflApiClient();
|
|
4126
|
-
if (!query?.competition
|
|
4450
|
+
if (!query?.competition) {
|
|
4127
4451
|
const [menResult, womenResult] = await Promise.all([
|
|
4128
|
-
client.fetchTeams("
|
|
4129
|
-
client.fetchTeams("
|
|
4452
|
+
client.fetchTeams("AFLM"),
|
|
4453
|
+
client.fetchTeams("AFLW")
|
|
4130
4454
|
]);
|
|
4131
4455
|
if (!menResult.success) return menResult;
|
|
4132
4456
|
if (!womenResult.success) return womenResult;
|
|
4133
4457
|
return ok([...toTeams(menResult.data, "AFLM"), ...toTeams(womenResult.data, "AFLW")]);
|
|
4134
4458
|
}
|
|
4135
|
-
const
|
|
4136
|
-
const teamType = query?.teamType ?? teamTypeForComp(competition);
|
|
4137
|
-
const result = await client.fetchTeams(teamType);
|
|
4459
|
+
const result = await client.fetchTeams(query.competition);
|
|
4138
4460
|
if (!result.success) return result;
|
|
4139
|
-
return ok(toTeams(result.data, competition));
|
|
4461
|
+
return ok(toTeams(result.data, query.competition));
|
|
4140
4462
|
}
|
|
4141
4463
|
async function fetchSquad(query) {
|
|
4142
|
-
const
|
|
4143
|
-
const
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
if (Number.isNaN(teamId)) {
|
|
4148
|
-
return err(new ValidationError(`Invalid team ID: ${query.teamId}`));
|
|
4149
|
-
}
|
|
4150
|
-
const squadResult = await client.fetchSquad(teamId, seasonResult.data);
|
|
4151
|
-
if (!squadResult.success) return squadResult;
|
|
4152
|
-
const players = squadResult.data.squad.players.map((p) => ({
|
|
4153
|
-
playerId: p.player.providerId ?? String(p.player.id),
|
|
4154
|
-
givenName: p.player.firstName,
|
|
4155
|
-
surname: p.player.surname,
|
|
4156
|
-
displayName: `${p.player.firstName} ${p.player.surname}`,
|
|
4157
|
-
jumperNumber: p.jumperNumber ?? null,
|
|
4158
|
-
position: p.position ?? null,
|
|
4159
|
-
dateOfBirth: p.player.dateOfBirth ? parseDate(p.player.dateOfBirth) : null,
|
|
4160
|
-
heightCm: p.player.heightInCm || null,
|
|
4161
|
-
weightKg: p.player.weightInKg || null,
|
|
4162
|
-
draftYear: p.player.draftYear ? Number.parseInt(p.player.draftYear, 10) || null : null,
|
|
4163
|
-
draftPosition: p.player.draftPosition ? Number.parseInt(p.player.draftPosition, 10) || null : null,
|
|
4164
|
-
draftType: p.player.draftType ?? null,
|
|
4165
|
-
debutYear: p.player.debutYear ? Number.parseInt(p.player.debutYear, 10) || null : null,
|
|
4166
|
-
recruitedFrom: p.player.recruitedFrom ?? null
|
|
4167
|
-
}));
|
|
4168
|
-
return ok({
|
|
4169
|
-
teamId: query.teamId,
|
|
4170
|
-
teamName: normaliseTeamName(squadResult.data.squad.team?.name ?? query.teamId),
|
|
4171
|
-
season: query.season,
|
|
4172
|
-
players,
|
|
4173
|
-
competition
|
|
4464
|
+
const source = query.source ?? squadRegistry.defaultSource;
|
|
4465
|
+
const adapterR = dispatch(squadRegistry, "squad", {
|
|
4466
|
+
source,
|
|
4467
|
+
competition: query.competition,
|
|
4468
|
+
season: query.season
|
|
4174
4469
|
});
|
|
4470
|
+
return Result.flatMapAsync(adapterR, (a) => a.fetchSquad({ ...query, source }));
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
// src/api/player-details.ts
|
|
4474
|
+
async function fetchPlayerDetails(query) {
|
|
4475
|
+
const competition = query.competition ?? "AFLM";
|
|
4476
|
+
if (query.team) {
|
|
4477
|
+
const squadR = await fetchSquad({
|
|
4478
|
+
team: query.team,
|
|
4479
|
+
season: query.season ?? (/* @__PURE__ */ new Date()).getFullYear(),
|
|
4480
|
+
source: query.source,
|
|
4481
|
+
competition
|
|
4482
|
+
});
|
|
4483
|
+
if (!squadR.success) return squadR;
|
|
4484
|
+
return ok(squadToPlayerDetails(squadR.data, query.source));
|
|
4485
|
+
}
|
|
4486
|
+
const teamNames = [...AFL_SENIOR_TEAMS];
|
|
4487
|
+
const results = await batchedMap(
|
|
4488
|
+
teamNames,
|
|
4489
|
+
(team) => fetchSquad({
|
|
4490
|
+
team,
|
|
4491
|
+
season: query.season ?? (/* @__PURE__ */ new Date()).getFullYear(),
|
|
4492
|
+
source: query.source,
|
|
4493
|
+
competition
|
|
4494
|
+
})
|
|
4495
|
+
);
|
|
4496
|
+
const allPlayers = [];
|
|
4497
|
+
for (const result of results) {
|
|
4498
|
+
if (result.success) {
|
|
4499
|
+
allPlayers.push(...squadToPlayerDetails(result.data, query.source));
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
return ok(allPlayers);
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
// src/api/team-stats.ts
|
|
4506
|
+
async function fetchTeamStats(query) {
|
|
4507
|
+
const adapterR = dispatch(teamStatsRegistry, "team stats", { ...query, competition: "AFLM" });
|
|
4508
|
+
return Result.flatMapAsync(adapterR, (a) => a.fetchTeamStats(query));
|
|
4175
4509
|
}
|
|
4176
4510
|
export {
|
|
4177
4511
|
AflApiClient,
|
|
@@ -4221,11 +4555,9 @@ export {
|
|
|
4221
4555
|
computeLadder,
|
|
4222
4556
|
err,
|
|
4223
4557
|
fetchAwards,
|
|
4224
|
-
fetchCoachesVotes,
|
|
4225
|
-
fetchFixture,
|
|
4226
4558
|
fetchLadder,
|
|
4227
4559
|
fetchLineup,
|
|
4228
|
-
|
|
4560
|
+
fetchMatches,
|
|
4229
4561
|
fetchPlayerDetails,
|
|
4230
4562
|
fetchPlayerStats,
|
|
4231
4563
|
fetchSquad,
|