/** * Cliente IGDB (Twitch OAuth) * - `searchGames(query, platform?)` * - `getGameById(id)` */ import { fetch } from 'undici'; export type MetadataGame = { id?: number; name: string; slug?: string; releaseDate?: string; genres?: string[]; platforms?: any[]; coverUrl?: string; source?: string; }; const AUTH_URL = 'https://id.twitch.tv/oauth2/token'; const API_URL = 'https://api.igdb.com/v4'; let cachedToken: { token: string; expiresAt: number } | null = null; async function getToken(): Promise { if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.token; const clientId = process.env.IGDB_CLIENT_ID || process.env.TWITCH_CLIENT_ID; const clientSecret = process.env.IGDB_CLIENT_SECRET || process.env.TWITCH_CLIENT_SECRET; if (!clientId || !clientSecret) return null; try { const params = new URLSearchParams({ client_id: clientId, client_secret: clientSecret, grant_type: 'client_credentials', }); const res = await fetch(`${AUTH_URL}?${params.toString()}`, { method: 'POST' }); if (!res.ok) return null; const json = await res.json(); const token = json.access_token as string | undefined; const expires = Number(json.expires_in) || 0; if (!token) return null; cachedToken = { token, expiresAt: Date.now() + Math.max(0, expires - 60) * 1000 }; return token; } catch (err) { // eslint-disable-next-line no-console console.debug('igdbClient.getToken error', err); return null; } } function mapIgdbHit(r: any): MetadataGame { return { id: r.id, name: r.name, slug: r.slug, releaseDate: r.first_release_date ? new Date(r.first_release_date * 1000).toISOString() : undefined, genres: Array.isArray(r.genres) ? r.genres : undefined, platforms: Array.isArray(r.platforms) ? r.platforms : undefined, coverUrl: r.cover?.url ?? undefined, source: 'igdb', }; } export async function searchGames(query: string, _platform?: string): Promise { const clientId = process.env.IGDB_CLIENT_ID || process.env.TWITCH_CLIENT_ID; const token = await getToken(); if (!clientId || !token) return []; const headers = { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json', 'Content-Type': 'text/plain', } as Record; const body = `search "${query}"; fields id,name,slug,first_release_date,genres,platforms,cover; limit 10;`; try { const res = await fetch(`${API_URL}/games`, { method: 'POST', headers, body }); if (!res.ok) return []; const json = await res.json(); if (!Array.isArray(json)) return []; return json.map(mapIgdbHit); } catch (err) { // eslint-disable-next-line no-console console.debug('igdbClient.searchGames error', err); return []; } } export async function getGameById(id: number): Promise { const clientId = process.env.IGDB_CLIENT_ID || process.env.TWITCH_CLIENT_ID; const token = await getToken(); if (!clientId || !token) return null; const headers = { 'Client-ID': clientId, Authorization: `Bearer ${token}`, Accept: 'application/json', 'Content-Type': 'text/plain', } as Record; const body = `where id = ${id}; fields id,name,slug,first_release_date,genres,platforms,cover; limit 1;`; try { const res = await fetch(`${API_URL}/games`, { method: 'POST', headers, body }); if (!res.ok) return null; const json = await res.json(); if (!Array.isArray(json) || json.length === 0) return null; return mapIgdbHit(json[0]); } catch (err) { // eslint-disable-next-line no-console console.debug('igdbClient.getGameById error', err); return null; } } /** * Metadatos: * Autor: GitHub Copilot * Última actualización: 2026-02-11 */