Files
wow-gear-finder/scripts/fetch-item-data.js

314 lines
8.1 KiB
JavaScript

#!/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<Object|null>} 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><!\[CDATA\[(.*)\]\]><\/json>/);
const jsonData = JSON.parse(`{${jsonString[1]}}`);
// Parse the XML response from Wowhead
// const nameMatch = data.match(/<name><!\[CDATA\[(.*?)\]\]><\/name>/);
const iconMatch = data.match(
/<icon displayId="(\d+)">(.*?)<\/icon>/,
);
const slotMatch = data.match(
/<inventorySlot id="(\d+)">(.*?)<\/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<Object|null>} 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<Object[]>} 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<number[]>} 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,
};