@hardbulls/wbsc-crawler
Version:
Tool to crawl events, leagues and statistics from WBSC based websites.
256 lines (255 loc) • 9.99 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LivescoreCrawler = void 0;
const date_fns_tz_1 = require("date-fns-tz");
const GameStatus_1 = require("./Model/GameStatus");
const fetch_1 = require("./fetch");
const LIVESCORES_URL = "https://game.wbsc.org/gamedata/livescores.json";
const GAMEDATA_BASE_URL = "https://game.wbsc.org/gamedata";
function parseInning(raw) {
const match = raw.match(/^([TB])(\d+)/);
if (!match) {
return null;
}
return {
half: match[1] === "T" ? "top" : "bottom",
number: parseInt(match[2], 10),
};
}
function mapStatus(raw) {
const code = parseInt(raw, 10);
if (code === 1)
return GameStatus_1.GameStatus.ONGOING;
if (code === 2 || code === 3)
return GameStatus_1.GameStatus.FINISHED;
if (code === 4)
return GameStatus_1.GameStatus.FORFEIT;
if (code === -2)
return GameStatus_1.GameStatus.SUSPENDED;
if (code === -3)
return GameStatus_1.GameStatus.CANCELED;
return GameStatus_1.GameStatus.SCHEDULED;
}
function mapLivescore(raw) {
var _a, _b;
const inning = parseInning(raw.inning);
return {
gameId: raw.gameid,
tournamentId: raw.tournamentid,
inning: (_a = inning === null || inning === void 0 ? void 0 : inning.number) !== null && _a !== void 0 ? _a : null,
inningHalf: (_b = inning === null || inning === void 0 ? void 0 : inning.half) !== null && _b !== void 0 ? _b : null,
homeRuns: parseInt(raw.homeruns, 10) || 0,
awayRuns: parseInt(raw.awayruns, 10) || 0,
balls: parseInt(raw.balls, 10) || 0,
strikes: parseInt(raw.strikes, 10) || 0,
outs: parseInt(raw.outs, 10) || 0,
runner1: raw.runner1 === "1",
runner2: raw.runner2 === "1",
runner3: raw.runner3 === "1",
pitcher: raw.pitcher,
batter: raw.batter,
status: mapStatus(raw.status),
start: (0, date_fns_tz_1.fromZonedTime)(raw.start, raw.start_tz),
};
}
function resolveRunner(raw, boxscore) {
var _a, _b;
if (!raw || raw === 0 || raw === "0")
return null;
return (_b = (_a = boxscore[String(raw)]) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : null;
}
function mapInningHalf(currentInning) {
const upper = currentInning.toUpperCase();
if (upper.startsWith("TOP"))
return "top";
if (upper.startsWith("BOT"))
return "bottom";
return null;
}
function parseBatterRecord(raw) {
const match = raw.match(/^(\d+) for (\d+)/);
return match
? { hits: parseInt(match[1], 10), atBats: parseInt(match[2], 10) }
: { hits: 0, atBats: 0 };
}
function mapPitchOutcome(r1) {
switch (r1) {
case "2":
return "ball";
case "4":
return "calledStrike";
case "5":
return "swingingStrike";
case "6":
return "foul";
case "9":
return "inPlay";
default:
return "unknown";
}
}
function mapPitchType(type) {
if (type === 1)
return "ball";
if (type === 2)
return "strike";
if (type === 3)
return "inPlay";
return "event";
}
function mapLineup(boxscore, teamCode) {
const entries = Object.entries(boxscore)
.filter(([, e]) => e.teamcode === teamCode)
.sort(([a], [b]) => parseInt(a, 10) - parseInt(b, 10))
.map(([, e]) => e);
const batters = entries
.filter((e) => e.POS !== undefined)
.map((e) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
return ({
name: e.name,
playerId: e.playerid,
image: e.image,
position: e.POS,
plateAppearances: (_a = e.PA) !== null && _a !== void 0 ? _a : 0,
atBats: (_b = e.AB) !== null && _b !== void 0 ? _b : 0,
runs: (_c = e.R) !== null && _c !== void 0 ? _c : 0,
hits: (_d = e.H) !== null && _d !== void 0 ? _d : 0,
rbi: (_e = e.RBI) !== null && _e !== void 0 ? _e : 0,
walks: (_f = e.BB) !== null && _f !== void 0 ? _f : 0,
strikeouts: (_g = e.SO) !== null && _g !== void 0 ? _g : 0,
average: (_h = e.AVG) !== null && _h !== void 0 ? _h : ".000",
});
});
const pitchers = entries
.filter((e) => e.PITCHIP !== undefined)
.map((e) => {
var _a, _b, _c, _d, _e;
return ({
name: e.name,
playerId: e.playerid,
image: e.image,
inningsPitched: e.PITCHIP,
earnedRuns: (_a = e.PITCHER) !== null && _a !== void 0 ? _a : 0,
hitsAllowed: (_b = e.PITCHH) !== null && _b !== void 0 ? _b : 0,
walksAllowed: (_c = e.PITCHBB) !== null && _c !== void 0 ? _c : 0,
strikeouts: (_d = e.PITCHSO) !== null && _d !== void 0 ? _d : 0,
earnedRunAverage: (_e = e.ERA) !== null && _e !== void 0 ? _e : "0.00",
});
});
return { batters, pitchers };
}
function mapLivePlay(raw) {
var _a, _b;
const pitchSequence = [...raw.platecount]
.reverse()
.map((entry) => ({
type: mapPitchType(entry.type),
description: entry.label,
}));
const situation = {
currentInning: raw.situation.currentinning,
inning: parseInt(raw.situation.inning, 10) || 0,
inningHalf: mapInningHalf(raw.situation.currentinning),
batter: { name: raw.situation.batter, playerId: raw.situation.batterid },
batterRecord: parseBatterRecord(raw.situation.batting),
batterAverage: raw.situation.avg,
pitcher: { name: raw.situation.pitcher, playerId: raw.situation.pitcherid },
earnedRunAverage: raw.situation.pitcherera,
inningsPitched: raw.situation.pitcherip,
pitchSequence,
runner1: resolveRunner(raw.situation.runner1, raw.boxscore),
runner2: resolveRunner(raw.situation.runner2, raw.boxscore),
runner3: resolveRunner(raw.situation.runner3, raw.boxscore),
outs: raw.situation.outs,
balls: raw.situation.balls,
strikes: raw.situation.strikes,
};
let currentHalf = null;
const plays = [...raw.playdata]
.reverse()
.reduce((acc, entry) => {
if (entry.p === 0 || entry.p === "0") {
if (entry.n.includes("***TOP"))
currentHalf = "top";
else if (entry.n.includes("***BOT"))
currentHalf = "bottom";
return acc;
}
acc.push({
description: entry.n,
inning: parseInt(entry.i, 10) || 0,
inningHalf: currentHalf,
pitchOutcome: mapPitchOutcome(entry.r1),
});
return acc;
}, []);
return {
lastPlay: raw.lastplayloaded,
gameId: raw.gameid,
gameOver: raw.gameover !== 0,
eventLocation: raw.eventlocation,
eventHome: raw.eventhome,
eventAway: raw.eventaway,
eventHomeId: raw.eventhomeid,
eventAwayId: raw.eventawayid,
regulationInnings: parseInt(raw.innings, 10),
situation,
homeLineup: mapLineup(raw.boxscore, raw.eventhome),
awayLineup: mapLineup(raw.boxscore, raw.eventaway),
linescore: {
awayRunsByInning: raw.linescore.awayruns,
homeRunsByInning: raw.linescore.homeruns,
awayTotals: {
runs: raw.linescore.awaytotals.R,
hits: raw.linescore.awaytotals.H,
errors: raw.linescore.awaytotals.E,
},
homeTotals: {
runs: raw.linescore.hometotals.R,
hits: raw.linescore.hometotals.H,
errors: raw.linescore.hometotals.E,
},
},
plays,
lastPlayDescription: (_b = (_a = raw.playdata[0]) === null || _a === void 0 ? void 0 : _a.n) !== null && _b !== void 0 ? _b : "",
};
}
function fetchLivePlay(gameId) {
return __awaiter(this, void 0, void 0, function* () {
const latestRes = yield (0, fetch_1.fetchUrl)(`${GAMEDATA_BASE_URL}/${gameId}/latest.json`, { method: "GET" });
const latestPlay = yield latestRes.json();
const playRes = yield (0, fetch_1.fetchUrl)(`${GAMEDATA_BASE_URL}/${gameId}/play${latestPlay}.json`, { method: "GET" });
const raw = yield playRes.json();
return mapLivePlay(raw);
});
}
exports.LivescoreCrawler = {
crawl: () => __awaiter(void 0, void 0, void 0, function* () {
const response = yield (0, fetch_1.fetchUrl)(LIVESCORES_URL, { method: "GET" });
const data = yield response.json();
return data.map(mapLivescore);
}),
crawlPlay: (gameId) => __awaiter(void 0, void 0, void 0, function* () {
return fetchLivePlay(gameId);
}),
crawlByGameId: (gameId) => __awaiter(void 0, void 0, void 0, function* () {
const response = yield (0, fetch_1.fetchUrl)(LIVESCORES_URL, { method: "GET" });
const data = yield response.json();
const raw = data.find((entry) => entry.gameid === gameId);
if (!raw)
return null;
const livescore = mapLivescore(raw);
livescore.play = yield fetchLivePlay(gameId);
return livescore;
}),
};