const DEFAULT_POKEMON_TCG_DATA_BASE = 'https://raw.githubusercontent.com/PokemonTCG/pokemon-tcg-data/master'; export function getPokemonTcgDataBaseUrl() { return process.env.POKEMON_TCG_DATA_BASE_URL || DEFAULT_POKEMON_TCG_DATA_BASE; } export function pokemonSetsUrl() { return `${getPokemonTcgDataBaseUrl()}/sets/en.json`; } export function pokemonSetCardsUrl(setCode) { return `${getPokemonTcgDataBaseUrl()}/cards/en/${encodeURIComponent(setCode)}.json`; } const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); function githubHeaders() { return { 'User-Agent': 'Deck-Hearth/1.0', Accept: 'application/json', }; } export async function fetchWithRetry(url, maxRetries = 3, delayMs = 2000) { for (let attempt = 1; attempt <= maxRetries; attempt += 1) { try { const response = await fetch(url, { headers: githubHeaders() }); if (response.ok) { return response; } if (response.status === 404) { throw new Error(`Resource not found: ${response.status}`); } if (response.status === 504 || response.status === 503 || response.status === 429) { console.log( `[fetchWithRetry] Attempt ${attempt}: ${response.status}, retrying in ${delayMs * attempt}ms` ); await delay(delayMs * attempt); continue; } throw new Error(`Pokemon TCG data fetch error: ${response.status}`); } catch (error) { if (attempt === maxRetries) { throw error; } console.log(`[fetchWithRetry] Attempt ${attempt} failed:`, error.message); await delay(delayMs * attempt); } } throw new Error('fetchWithRetry exhausted retries'); } export async function fetchPokemonSets() { const response = await fetchWithRetry(pokemonSetsUrl()); const sets = await response.json(); return Array.isArray(sets) ? sets : []; } export async function fetchPokemonSetCards(setCode) { try { const response = await fetchWithRetry(pokemonSetCardsUrl(setCode)); const cards = await response.json(); return Array.isArray(cards) ? cards : []; } catch (error) { if (error.message.includes('404')) { console.warn(`[fetchPokemonSetCards] No card file for set ${setCode} in pokemon-tcg-data`); return []; } throw error; } } export function normalizePokemonRarity(rarity) { const value = rarity || 'Unknown'; if (value.includes('Holo')) return 'Holographic'; if (value.includes('Secret')) return 'Secret Rare'; if (value.includes('Ultra')) return 'Ultra Rare'; return value; } /** * Map a pokemon-tcg-data card JSON object into DB insert fields. */ export function mapGithubCardForInsert(card, setMeta) { const setCode = setMeta?.id || null; const setName = setMeta?.name || setCode; const printedTotal = setMeta?.printedTotal || setMeta?.total || null; const cardNumber = card.number && printedTotal ? `${card.number}/${printedTotal}` : card.number || null; return { externalId: card.id, name: card.name, setName, setCode, cardNumber, rarity: normalizePokemonRarity(card.rarity), cardType: card.supertype || 'Pokemon', types: card.types || [], flavorText: card.flavorText || null, hp: card.hp || card.attacks?.[0]?.damage || null, imageSmall: card.images?.small || null, imageLarge: card.images?.large || null, }; }