Fetches better item data, adds spec selector
This commit is contained in:
241
scripts/blizzard-api.js
Normal file
241
scripts/blizzard-api.js
Normal 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
|
||||
};
|
||||
Reference in New Issue
Block a user