Fetches better item data, adds spec selector

This commit is contained in:
2025-08-25 10:10:35 -07:00
parent b5ae96870f
commit 181805fcab
30 changed files with 4864 additions and 592 deletions

241
scripts/blizzard-api.js Normal file
View File

@@ -0,0 +1,241 @@
#!/usr/bin/env node
import https from 'https';
/**
* Blizzard API client for fetching item data
* To use this, you'll need to:
* 1. Create an app at https://develop.battle.net/
* 2. Get your client ID and client secret
* 3. Set environment variables: BLIZZARD_CLIENT_ID and BLIZZARD_CLIENT_SECRET
*/
let accessToken = null;
let tokenExpiry = null;
/**
* Gets an access token from Blizzard API
* @returns {Promise<string|null>} Access token or null if failed
*/
async function getAccessToken() {
const clientId = process.env.BLIZZARD_CLIENT_ID;
const clientSecret = process.env.BLIZZARD_CLIENT_SECRET;
if (!clientId || !clientSecret) {
console.error('Blizzard API credentials not found. Please set BLIZZARD_CLIENT_ID and BLIZZARD_CLIENT_SECRET environment variables.');
return null;
}
// Check if we have a valid token
if (accessToken && tokenExpiry && Date.now() < tokenExpiry) {
return accessToken;
}
return new Promise((resolve) => {
const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const postData = 'grant_type=client_credentials';
const options = {
hostname: 'oauth.battle.net',
port: 443,
path: '/token',
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response.access_token) {
accessToken = response.access_token;
tokenExpiry = Date.now() + (response.expires_in * 1000);
resolve(accessToken);
} else {
console.error('Failed to get access token:', response);
resolve(null);
}
} catch (error) {
console.error('Error parsing token response:', error.message);
resolve(null);
}
});
});
req.on('error', (error) => {
console.error('Error getting access token:', error.message);
resolve(null);
});
req.write(postData);
req.end();
});
}
/**
* Fetches item data from Blizzard API
* @param {number} itemId - The item ID to fetch
* @param {string} region - The region (default: 'us')
* @returns {Promise<Object|null>} Item data or null if failed
*/
async function fetchFromBlizzard(itemId, region = 'us') {
const token = await getAccessToken();
if (!token) {
return null;
}
return new Promise((resolve) => {
const options = {
hostname: `${region}.api.blizzard.com`,
port: 443,
path: `/data/wow/item/${itemId}?namespace=static-${region}&locale=en_US&access_token=${token}`,
method: 'GET'
};
https.get(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response.name) {
const itemData = {
id: itemId,
name: response.name,
slot: mapSlotFromBlizzard(response.inventory_type?.type),
quality: response.quality?.name?.toLowerCase(),
icon: response.media ? `blizzard-${response.media.id}` : null,
source: 'blizzard',
blizzardData: {
itemClass: response.item_class?.name,
itemSubclass: response.item_subclass?.name,
inventoryType: response.inventory_type?.name,
bindType: response.bind_type?.name,
level: response.level,
requiredLevel: response.required_level
}
};
resolve(itemData);
} else {
console.log(`No data found for item ${itemId} on Blizzard API`);
resolve(null);
}
} catch (error) {
console.error(`Error parsing Blizzard data for item ${itemId}:`, error.message);
resolve(null);
}
});
}).on('error', (error) => {
console.error(`Error fetching from Blizzard for item ${itemId}:`, error.message);
resolve(null);
});
});
}
/**
* Maps Blizzard inventory types to our slot names
* @param {string} inventoryType - Blizzard inventory type
* @returns {string|null} Our slot name or null
*/
function mapSlotFromBlizzard(inventoryType) {
const typeMap = {
'HEAD': 'head',
'NECK': 'neck',
'SHOULDER': 'shoulders',
'BODY': 'chest',
'CHEST': 'chest',
'WAIST': 'waist',
'LEGS': 'legs',
'FEET': 'feet',
'WRIST': 'wrist',
'HAND': 'hands',
'FINGER': 'finger',
'TRINKET': 'trinket',
'WEAPON': '1h-weapon',
'SHIELD': 'off-hand',
'RANGED': '2h-weapon',
'CLOAK': 'back',
'WEAPON_2H': '2h-weapon',
'WEAPON_MAIN_HAND': '1h-weapon',
'WEAPON_OFF_HAND': 'off-hand',
'HOLDABLE': 'off-hand',
'AMMO': null,
'THROWN': '1h-weapon',
'RANGED_RIGHT': '2h-weapon',
'QUIVER': null,
'RELIC': 'trinket'
};
return typeMap[inventoryType] || null;
}
/**
* Fetches item media (icon) from Blizzard API
* @param {number} itemId - The item ID
* @param {string} region - The region (default: 'us')
* @returns {Promise<string|null>} Icon URL or null if failed
*/
async function fetchItemMedia(itemId, region = 'us') {
const token = await getAccessToken();
if (!token) {
return null;
}
return new Promise((resolve) => {
const options = {
hostname: `${region}.api.blizzard.com`,
port: 443,
path: `/data/wow/media/item/${itemId}?namespace=static-${region}&locale=en_US&access_token=${token}`,
method: 'GET'
};
https.get(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response.assets && response.assets.length > 0) {
const iconAsset = response.assets.find(asset => asset.key === 'icon');
resolve(iconAsset ? iconAsset.value : null);
} else {
resolve(null);
}
} catch (error) {
console.error(`Error parsing media data for item ${itemId}:`, error.message);
resolve(null);
}
});
}).on('error', (error) => {
console.error(`Error fetching media for item ${itemId}:`, error.message);
resolve(null);
});
});
}
export {
fetchFromBlizzard,
fetchItemMedia,
getAccessToken,
mapSlotFromBlizzard
};