deckhearth/scripts/populate-card-database.js

393 lines
No EOL
11 KiB
JavaScript

#!/usr/bin/env node
/**
* Card Database Population Script
*
* This script fetches cards from external APIs and adds them to your local database.
* Run this to populate your database with popular cards from various games.
*/
const fetch = require('node-fetch');
// Configuration
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
const AUTH_TOKEN = process.env.TCG_VAULT_TOKEN; // Set this in your environment
// Popular cards to fetch (examples)
const POPULAR_CARDS = {
MTG: [
'Lightning Bolt',
'Black Lotus',
'Counterspell',
'Dark Ritual',
'Brainstorm',
'Force of Will',
'Wasteland',
'Tarmogoyf',
'Snapcaster Mage',
'Jace, the Mind Sculptor'
],
POKEMON: [
'Pikachu',
'Charizard',
'Blastoise',
'Venusaur',
'Mewtwo',
'Lugia',
'Ho-Oh',
'Rayquaza',
'Garchomp',
'Lucario'
],
YUGIOH: [
'Blue-Eyes White Dragon',
'Dark Magician',
'Red-Eyes Black Dragon',
'Exodia the Forbidden One',
'Slifer the Sky Dragon',
'Obelisk the Tormentor',
'The Winged Dragon of Ra',
'Elemental HERO Neos',
'Stardust Dragon',
'Number 39: Utopia'
],
LORCANA: [
'Mickey Mouse - Brave Little Tailor',
'Elsa - Snow Queen',
'Be Prepared',
'Mickey Mouse - Steamboat Pilot',
'Maleficent - Dragon Form',
'Aurora - Briar Rose',
'Stitch - Experiment 626',
'Aladdin - Street Rat',
'Cinderella - Ballroom Sensation',
'Belle - Strange but Special'
]
};
// Rate limiting utility
class RateLimiter {
constructor(maxRequests, timeWindow) {
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
this.requests = [];
}
async waitForSlot() {
const now = Date.now();
this.requests = this.requests.filter(time => now - time < this.timeWindow);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.timeWindow - (now - oldestRequest);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.requests.push(now);
}
}
const scryfallLimiter = new RateLimiter(10, 1000); // 10 requests per second
const pokemonLimiter = new RateLimiter(5, 1000); // 5 requests per second
const lorcastLimiter = new RateLimiter(10, 1000); // 10 requests per second (50-100ms delay)
// API Services
const mtgService = {
async getCardByName(name) {
await scryfallLimiter.waitForSlot();
try {
const response = await fetch(
`https://api.scryfall.com/cards/named?fuzzy=${encodeURIComponent(name)}`
);
if (!response.ok) {
console.log(`❌ MTG card not found: ${name}`);
return null;
}
const card = await response.json();
return {
name: card.name,
game: 'MTG',
set_name: card.set_name,
set_code: card.set,
card_number: card.collector_number,
rarity: card.rarity,
mana_cost: card.mana_cost,
cmc: card.cmc,
card_type: card.type_line,
colors: card.colors,
oracle_text: card.oracle_text,
power: card.power,
toughness: card.toughness,
image_url: card.image_uris?.normal,
stock_image_url: card.image_uris?.normal,
current_price: card.prices?.usd ? parseFloat(card.prices.usd) : null,
market_price: card.prices?.usd_foil ? parseFloat(card.prices.usd_foil) : null,
};
} catch (error) {
console.error(`❌ Error fetching MTG card ${name}:`, error.message);
return null;
}
}
};
const pokemonService = {
async getCardByName(name) {
await pokemonLimiter.waitForSlot();
try {
const response = await fetch(
`https://api.pokemontcg.io/v2/cards?q=name:${encodeURIComponent(name)}&pageSize=1`
);
if (!response.ok) {
console.log(`❌ Pokémon card not found: ${name}`);
return null;
}
const data = await response.json();
if (data.data.length === 0) {
console.log(`❌ Pokémon card not found: ${name}`);
return null;
}
const card = data.data[0];
return {
name: card.name,
game: 'POKEMON',
set_name: card.set.name,
set_code: card.set.id,
card_number: card.number,
rarity: card.rarity,
mana_cost: card.convertedRetreatCost?.toString(),
cmc: card.convertedRetreatCost,
card_type: card.supertype,
colors: card.types || [],
oracle_text: card.attacks?.map(attack => `${attack.name}: ${attack.text}`).join('\n'),
power: card.hp,
toughness: null,
image_url: card.images.small,
stock_image_url: card.images.large,
current_price: card.cardmarket?.prices?.averageSellPrice || null,
market_price: card.cardmarket?.prices?.lowPrice || null,
};
} catch (error) {
console.error(`❌ Error fetching Pokémon card ${name}:`, error.message);
return null;
}
}
};
const yugiohService = {
async getCardByName(name) {
try {
const response = await fetch(
`https://db.ygoprodeck.com/api/v7/cardinfo.php?fname=${encodeURIComponent(name)}`
);
if (!response.ok) {
console.log(`❌ Yu-Gi-Oh! card not found: ${name}`);
return null;
}
const data = await response.json();
if (data.data.length === 0) {
console.log(`❌ Yu-Gi-Oh! card not found: ${name}`);
return null;
}
const card = data.data[0];
return {
name: card.name,
game: 'YUGIOH',
set_name: card.set_name,
set_code: card.set_code,
card_number: card.num,
rarity: card.rarity,
mana_cost: card.level?.toString(),
cmc: card.level,
card_type: card.type,
colors: [card.attribute],
oracle_text: card.desc,
power: card.atk?.toString(),
toughness: card.def?.toString(),
image_url: card.card_images?.[0]?.image_url,
stock_image_url: card.card_images?.[0]?.image_url_small,
current_price: card.card_prices?.[0]?.amazon_price ? parseFloat(card.card_prices[0].amazon_price) : null,
market_price: card.card_prices?.[0]?.cardmarket_price ? parseFloat(card.card_prices[0].cardmarket_price) : null,
};
} catch (error) {
console.error(`❌ Error fetching Yu-Gi-Oh! card ${name}:`, error.message);
return null;
}
}
};
const lorcanaService = {
async getCardByName(name) {
await lorcastLimiter.waitForSlot();
try {
const response = await fetch(
`https://api.lorcast.com/v0/cards?q=name:${encodeURIComponent(name)}`
);
if (!response.ok) {
console.log(`❌ Lorcana card not found: ${name}`);
return null;
}
const data = await response.json();
if (data.cards.length === 0) {
console.log(`❌ Lorcana card not found: ${name}`);
return null;
}
const card = data.cards[0];
return {
name: card.name,
game: 'LORCANA',
set_name: card.set?.name || '',
set_code: card.set?.code || '',
card_number: card.number,
rarity: card.rarity,
mana_cost: card.cost?.toString(),
cmc: card.cost,
card_type: card.type,
colors: card.colors || [],
oracle_text: card.text || '',
power: card.strength?.toString(),
toughness: card.willpower?.toString(),
image_url: card.image_url,
stock_image_url: card.image_url,
current_price: card.price?.market || null,
market_price: card.price?.low || null,
};
} catch (error) {
console.error(`❌ Error fetching Lorcana card ${name}:`, error.message);
return null;
}
}
};
// Database API
const databaseAPI = {
async addCard(cardData) {
try {
const response = await fetch(`${API_BASE_URL}/api/cards/find-or-create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(AUTH_TOKEN && { 'Authorization': `Bearer ${AUTH_TOKEN}` }),
},
body: JSON.stringify({
name: cardData.name,
game: cardData.game,
setName: cardData.set_name,
setCode: cardData.set_code,
rarity: cardData.rarity,
cardType: cardData.card_type,
manaCost: cardData.mana_cost,
imageUrl: cardData.stock_image_url,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error(`❌ Error adding card to database:`, error.message);
return null;
}
}
};
// Main execution
async function populateDatabase() {
console.log('🚀 Starting card database population...\n');
let totalProcessed = 0;
let totalAdded = 0;
let totalErrors = 0;
for (const [game, cards] of Object.entries(POPULAR_CARDS)) {
console.log(`📦 Processing ${game} cards...`);
for (const cardName of cards) {
totalProcessed++;
console.log(` 🔍 Fetching: ${cardName}`);
let cardData = null;
// Fetch card data based on game
switch (game) {
case 'MTG':
cardData = await mtgService.getCardByName(cardName);
break;
case 'POKEMON':
cardData = await pokemonService.getCardByName(cardName);
break;
case 'YUGIOH':
cardData = await yugiohService.getCardByName(cardName);
break;
case 'LORCANA':
cardData = await lorcanaService.getCardByName(cardName);
break;
}
if (cardData) {
console.log(` ✅ Found: ${cardData.name} (${cardData.set_name})`);
// Add to database
const result = await databaseAPI.addCard(cardData);
if (result && result.success) {
console.log(` 💾 Added to database: ${cardData.name}`);
totalAdded++;
} else {
console.log(` ⚠️ Already exists or failed to add: ${cardData.name}`);
totalErrors++;
}
} else {
console.log(` ❌ Not found: ${cardName}`);
totalErrors++;
}
// Small delay between requests
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log('');
}
console.log('📊 Population Summary:');
console.log(` Total processed: ${totalProcessed}`);
console.log(` Successfully added: ${totalAdded}`);
console.log(` Errors/not found: ${totalErrors}`);
console.log('\n✨ Database population complete!');
}
// Run the script
if (require.main === module) {
populateDatabase().catch(console.error);
}
module.exports = {
populateDatabase,
mtgService,
pokemonService,
yugiohService,
databaseAPI
};