88 lines
3.1 KiB
JavaScript
88 lines
3.1 KiB
JavaScript
/**
|
|
* Game-log service — fetches recent player game logs.
|
|
*
|
|
* Primary path: the Python FastAPI service at PYTHON_SERVICE_URL (default
|
|
* http://localhost:8000). Its /stats/last-n and /wnba/stats/last-n
|
|
* endpoints return per-game stat rows.
|
|
*
|
|
* Secondary path: not implemented in this session. If the Python service
|
|
* is unreachable, we return null and let the feature cache omit the
|
|
* features that depend on game logs. A flaky stats backend should NOT
|
|
* generate fake feature values.
|
|
*/
|
|
|
|
const axios = require('axios');
|
|
const { cacheGet, cacheSet } = require('../../utils/redis');
|
|
|
|
const PYTHON_BASE = process.env.PYTHON_SERVICE_URL || 'http://localhost:8000';
|
|
const CACHE_TTL_SECONDS = 4 * 60 * 60; // 4h — game logs change once per night
|
|
const HTTP_TIMEOUT_MS = 15_000;
|
|
|
|
function pythonPath(sport) {
|
|
switch (sport) {
|
|
case 'nba': return '/stats/last-n';
|
|
case 'wnba': return '/wnba/stats/last-n';
|
|
default: return null;
|
|
}
|
|
}
|
|
|
|
async function getGameLogs(playerName, sport, count = 20) {
|
|
const path = pythonPath(sport);
|
|
if (!path) return null;
|
|
const cacheKey = `gamelogs:${sport}:${playerName}:${count}`;
|
|
const cached = await cacheGet(cacheKey);
|
|
if (cached) return cached;
|
|
|
|
try {
|
|
const res = await axios.get(`${PYTHON_BASE}${path}`, {
|
|
params: { player: playerName, n: count },
|
|
timeout: HTTP_TIMEOUT_MS,
|
|
});
|
|
const games = res.data?.games || res.data?.results || [];
|
|
if (!Array.isArray(games) || games.length === 0) return null;
|
|
await cacheSet(cacheKey, games, CACHE_TTL_SECONDS);
|
|
return games;
|
|
} catch (err) {
|
|
// Python service down or returning 404 — return null, caller omits.
|
|
if (err?.response?.status !== 404) {
|
|
console.warn(`[gameLog] fetch failed for ${playerName}:`, err?.message);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Career playoff games — approximated from the season-avg endpoint's career
|
|
// summary, if present. If the Python service doesn't surface this, return
|
|
// null and let the caller skip the feature.
|
|
async function getCareerPlayoffGames(playerName, sport) {
|
|
if (sport !== 'nba' && sport !== 'wnba') return null;
|
|
try {
|
|
const res = await axios.get(`${PYTHON_BASE}/stats/season-avg`, {
|
|
params: { player: playerName, season: 'career' },
|
|
timeout: HTTP_TIMEOUT_MS,
|
|
});
|
|
const games = res.data?.career_playoff_games;
|
|
return Number.isFinite(Number(games)) ? Number(games) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// with/without analysis — compare a player's stats when a specific teammate
|
|
// is in vs out. Requires the Python service to expose this; if not, the
|
|
// feature falls back to a league-average bump (caller's choice).
|
|
async function getWithWithoutStats(playerName, sport, statType, teammateName) {
|
|
if (sport !== 'nba' && sport !== 'wnba') return null;
|
|
try {
|
|
const res = await axios.get(`${PYTHON_BASE}/stats/with-without`, {
|
|
params: { player: playerName, stat_type: statType, teammate: teammateName },
|
|
timeout: HTTP_TIMEOUT_MS,
|
|
});
|
|
return res.data || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
module.exports = { getGameLogs, getCareerPlayoffGames, getWithWithoutStats };
|