328 lines
No EOL
8.9 KiB
JavaScript
328 lines
No EOL
8.9 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'
|
|
]
|
|
};
|
|
|
|
// 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
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
|
|
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
|
|
};
|