deckhearth/lib/card-import/pokemon-github.js
varutasu 174a370fc3
Switch Pokémon catalog import to pokemon-tcg-data on GitHub. (#52)
Replace pokemontcg.io API discovery and import with raw JSON from PokemonTCG/pokemon-tcg-data; format collector numbers as number/printedTotal and drop the API key dependency for catalog sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 11:56:01 -05:00

111 lines
3.3 KiB
JavaScript

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