#!/usr/bin/env node import https from "https"; import fs from "fs/promises"; import path from "path"; /** * Fetches item data from Wowhead API * @param {number} itemId - The item ID to fetch * @returns {Promise} Item data or null if failed */ async function fetchFromWowhead(itemId, source) { return new Promise((resolve) => { const url = `https://www.wowhead.com/item=${itemId}&xml`; https .get(url, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { try { // Weirdly, the JSON in the XML has more data than the XML itself or the JSON API. const jsonString = data.match(/<\/json>/); const jsonData = JSON.parse(`{${jsonString[1]}}`); // Parse the XML response from Wowhead // const nameMatch = data.match(/<\/name>/); const iconMatch = data.match( /(.*?)<\/icon>/, ); const slotMatch = data.match( /(.*?)<\/inventorySlot>/, ); const name = jsonData.displayName; const itemData = { id: itemId, name, icon: iconMatch ? iconMatch[2] : null, slot: mapSlotFromWowhead(jsonData.slot), specs: jsonData.specs || [], source, }; resolve(itemData); } catch (error) { console.error( `Error parsing Wowhead data for item ${itemId}:`, error.message, ); resolve(null); } }); }) .on("error", (error) => { console.error( `Error fetching from Wowhead for item ${itemId}:`, error.message, ); resolve(null); }); }); } /** * Fetches item data from Wowhead's JSON API (alternative method) * @param {number} itemId - The item ID to fetch * @returns {Promise} Item data or null if failed */ async function fetchFromWowheadJson(itemId) { return new Promise((resolve) => { const url = `https://nether.wowhead.com/tooltip/item/${itemId}`; https .get(url, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { try { const jsonData = JSON.parse(data); if (jsonData && jsonData.name) { const itemData = { id: itemId, name: jsonData.name, icon: jsonData.icon || null, slot: mapSlotFromWowhead(jsonData.slot || 0), }; resolve(itemData); } else { resolve(null); } } catch (error) { console.error( `Error parsing Wowhead JSON data for item ${itemId}:`, error.message, ); resolve(null); } }); }) .on("error", (error) => { console.error( `Error fetching from Wowhead JSON for item ${itemId}:`, error.message, ); resolve(null); }); }); } /** * Maps Wowhead slot numbers to our slot names * @param {number} slotId - Wowhead slot ID * @returns {string|null} Our slot name or null */ function mapSlotFromWowhead(slotId) { const slotMap = { 1: "head", 2: "neck", 3: "shoulders", 5: "chest", 6: "waist", 7: "legs", 8: "feet", 9: "wrist", 10: "hands", 11: "finger", 12: "trinket", 13: "1h-weapon", 14: "off-hand", 15: "back", 16: "1h-weapon", // Main hand 17: "2h-weapon", 21: "1h-weapon", // One-handed 22: "off-hand", 23: "off-hand", // Held in off-hand 25: "1h-weapon", // Thrown }; return slotMap[slotId] || null; } /** * Fetches multiple items with rate limiting * @param {number[]} itemIds - Array of item IDs to fetch * @param {number} delay - Delay between requests in ms * @returns {Promise} Array of item data */ async function fetchMultipleItems(source, itemIds, delay = 1000) { const results = []; console.log(`Fetching data for ${itemIds.length} items...`); for (let i = 0; i < itemIds.length; i++) { const itemId = itemIds[i]; console.log(`Fetching item ${itemId} (${i + 1}/${itemIds.length})`); // Try Wowhead JSON API first, fallback to XML let itemData = await fetchFromWowhead(itemId, source); if (!itemData) { itemData = await fetchFromWowheadJson(itemId, source); } if (itemData) { results.push(itemData); console.log( `✓ Found: ${itemData.name} (${itemData.slot || "unknown slot"})`, ); } else { console.log(`✗ Failed to fetch data for item ${itemId}`); } // Rate limiting if (i < itemIds.length - 1) { await new Promise((resolve) => setTimeout(resolve, delay)); } } return results; } /** * Reads item IDs from existing drop files * @returns {Promise} Array of item IDs */ async function getExistingItemIds() { const dropsDir = path.join(process.cwd(), "src", "lib", "drops"); const itemIds = []; try { const files = await fs.readdir(dropsDir); for (const file of files) { if (file.endsWith(".ts") && file !== "index.ts") { const filePath = path.join(dropsDir, file); const content = await fs.readFile(filePath, "utf-8"); // Extract item IDs using regex const idMatches = content.match(/id: (\d+) as ItemId/g); if (idMatches) { for (const match of idMatches) { const id = parseInt(match.match(/id: (\d+)/)[1]); itemIds.push(id); } } } } } catch (error) { console.error("Error reading existing item IDs:", error.message); } return [...new Set(itemIds)]; // Remove duplicates } /** * Saves fetched data to a JSON file * @param {Object[]} items - Array of item data * @param {string} filename - Output filename */ async function saveToFile(items, filename = "fetched-items.json") { try { const outputPath = path.join(process.cwd(), filename); await fs.writeFile(outputPath, JSON.stringify(items, null, 2)); console.log(`\nSaved ${items.length} items to ${filename}`); } catch (error) { console.error("Error saving file:", error.message); } } /** * Main function */ async function main() { const args = process.argv.slice(2); if (args.length === 0) { console.log( "Usage: npm run fetch-items [item1] [item2] ... or npm run fetch-items --existing", ); console.log("Examples:"); console.log(" npm run fetch-items 242494 242495 242481"); console.log( " npm run fetch-items --existing # Fetch data for all items in drop files", ); return; } let itemIds = []; let source = null; if (args[0] === "--existing") { console.log("Fetching data for existing items in drop files..."); itemIds = await getExistingItemIds(); console.log(`Found ${itemIds.length} existing items`); } else { source = args[0]; itemIds = args .slice(1) .map((arg) => parseInt(arg)) .filter((id) => !isNaN(id)); } if (itemIds.length === 0) { console.log("No valid item IDs provided"); return; } const items = await fetchMultipleItems(source, itemIds); if (items.length > 0) { await saveToFile(items); console.log("\nSummary:"); console.log( `Successfully fetched: ${items.length}/${itemIds.length} items`, ); // Show slot distribution const slotCounts = {}; items.forEach((item) => { const slot = item.slot || "unknown"; slotCounts[slot] = (slotCounts[slot] || 0) + 1; }); console.log("\nSlot distribution:"); Object.entries(slotCounts).forEach(([slot, count]) => { console.log(` ${slot}: ${count}`); }); } else { console.log("No items were successfully fetched"); } } // Run the script if (import.meta.url === `file://${process.argv[1]}`) { main().catch(console.error); } export { fetchFromWowhead, fetchFromWowheadJson, fetchMultipleItems, getExistingItemIds, };