98 lines
3.2 KiB
JavaScript
98 lines
3.2 KiB
JavaScript
/**
|
|
* College Football Data (CFBD) — advanced college analytics.
|
|
*
|
|
* 100% free API key. Covers historical betting lines, player usage, team
|
|
* talent composites, advanced efficiency (PPA), recruiting. nba_api
|
|
* doesn't cover college; CFBD fills the gap for NCAAB and NCAAFB props.
|
|
*
|
|
* Note: CFBD's primary product is college football. College *basketball*
|
|
* coverage is via the /cbb endpoints. We expose both via the sport-aware
|
|
* functions below.
|
|
*/
|
|
|
|
const axios = require('axios');
|
|
const { createLimiter, createCircuitBreaker } = require('../../utils/rateLimiter');
|
|
const { cacheGet, cacheSet } = require('../../utils/redis');
|
|
|
|
const SOURCE = 'cfbd';
|
|
const HTTP_TIMEOUT_MS = 10_000;
|
|
const CACHE_TTL_SECONDS = 6 * 60 * 60; // 6h — most CFBD data is daily-fresh
|
|
|
|
const BASE_URL = process.env.CFBD_BASE_URL || 'https://api.collegefootballdata.com';
|
|
|
|
// Generous free tier — 10 req/min keeps us well under documented limits.
|
|
const limiter = createLimiter({ tokensPerInterval: 10, interval: 60_000 });
|
|
const breaker = createCircuitBreaker({ failureThreshold: 3, resetTimeout: 60_000 });
|
|
|
|
function configured() {
|
|
return !!process.env.CFBD_KEY;
|
|
}
|
|
|
|
async function fetchWithGuards(url, params, cacheKey) {
|
|
if (!configured()) return null;
|
|
const cached = await cacheGet(cacheKey);
|
|
if (cached) return cached;
|
|
|
|
await limiter.waitForToken();
|
|
try {
|
|
const data = await breaker.call(async () => {
|
|
const res = await axios.get(url, {
|
|
params,
|
|
headers: { Authorization: `Bearer ${process.env.CFBD_KEY}` },
|
|
timeout: HTTP_TIMEOUT_MS,
|
|
validateStatus: (s) => (s >= 200 && s < 300) || s === 429,
|
|
});
|
|
if (res.status === 429) {
|
|
const err = new Error('cfbd rate limited');
|
|
err.code = 'CFBD_429';
|
|
throw err;
|
|
}
|
|
return res.data;
|
|
});
|
|
await cacheSet(cacheKey, data, CACHE_TTL_SECONDS);
|
|
return data;
|
|
} catch (err) {
|
|
if (err?.code === 'CIRCUIT_OPEN') return null;
|
|
console.warn(`[${SOURCE}] fetch failed for ${cacheKey}:`, err?.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function getTeamStats(team, year) {
|
|
const cacheKey = `cfbd:teamstats:${team}:${year}`;
|
|
const data = await fetchWithGuards(`${BASE_URL}/stats/season`, { year, team }, cacheKey);
|
|
return Array.isArray(data) ? data : [];
|
|
}
|
|
|
|
async function getPlayerUsage(player, team, year) {
|
|
const cacheKey = `cfbd:usage:${player}:${team}:${year}`;
|
|
const data = await fetchWithGuards(
|
|
`${BASE_URL}/player/usage`,
|
|
{ year, team, player },
|
|
cacheKey
|
|
);
|
|
return Array.isArray(data) ? data : [];
|
|
}
|
|
|
|
async function getTalentComposite(team, year) {
|
|
const cacheKey = `cfbd:talent:${team}:${year}`;
|
|
const data = await fetchWithGuards(`${BASE_URL}/talent`, { year }, cacheKey);
|
|
if (!Array.isArray(data)) return null;
|
|
return data.find((row) => (row.school || row.team) === team) || null;
|
|
}
|
|
|
|
async function getHistoricalLines(team, year) {
|
|
const cacheKey = `cfbd:lines:${team}:${year}`;
|
|
const data = await fetchWithGuards(`${BASE_URL}/lines`, { year, team }, cacheKey);
|
|
return Array.isArray(data) ? data : [];
|
|
}
|
|
|
|
module.exports = {
|
|
configured,
|
|
getTeamStats,
|
|
getPlayerUsage,
|
|
getTalentComposite,
|
|
getHistoricalLines,
|
|
__internals: { limiter, breaker, BASE_URL },
|
|
};
|