Files
vyndr/src/services/intelligence/gameLogService.js
T

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 };