Skip to content

Commit 58eef52

Browse files
rainerstudiosclaude
andcommitted
Add CS2 Item Search API with daily cache refresh
Implements item search functionality backed by ByMykel's CSGO-API: **Features:** - Cached item search (2013 CS2 items) - Daily automatic cache refresh (24-hour TTL) - In-memory caching for fast responses - Fallback to stale cache if API unavailable **New Endpoints:** - GET /api/items/search?q=query&limit=20 - Search items by name - GET /api/items/:itemName - Get exact item by name - GET /api/items/cache/stats - View cache statistics - POST /api/items/cache/refresh - Force cache refresh **Technical Details:** - Uses node-fetch (already in dependencies) - Singleton pattern for cache management - Automatic hourly refresh check - Graceful error handling with stale cache fallback - Removed old/duplicate item search endpoint Data Source: https://github.com/ByMykel/CSGO-API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7c690ae commit 58eef52

2 files changed

Lines changed: 324 additions & 29 deletions

File tree

index.js

Lines changed: 125 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2711,35 +2711,35 @@ app.get('/api/items/metadata/:itemName', async (req, res) => {
27112711
}
27122712
});
27132713

2714-
// Search items
2715-
app.get('/api/items/search', async (req, res) => {
2716-
try {
2717-
const query = req.query.q || req.query.query;
2718-
const limit = parseInt(req.query.limit) || 20;
2719-
2720-
if (!query) {
2721-
return res.status(400).json({
2722-
error: 'Missing search query',
2723-
message: 'Provide ?q=query parameter'
2724-
});
2725-
}
2726-
2727-
const results = await csgoAPI.searchItems(query, limit);
2728-
2729-
res.json({
2730-
success: true,
2731-
query,
2732-
count: results.length,
2733-
results
2734-
});
2735-
} catch (error) {
2736-
winston.error('Item search error:', error);
2737-
res.status(500).json({
2738-
error: 'Failed to search items',
2739-
message: error.message
2740-
});
2741-
}
2742-
});
2714+
// OLD Search items endpoint - DEPRECATED (replaced by CS2 Item Cache version below)
2715+
// app.get('/api/items/search', async (req, res) => {
2716+
// try {
2717+
// const query = req.query.q || req.query.query;
2718+
// const limit = parseInt(req.query.limit) || 20;
2719+
//
2720+
// if (!query) {
2721+
// return res.status(400).json({
2722+
// error: 'Missing search query',
2723+
// message: 'Provide ?q=query parameter'
2724+
// });
2725+
// }
2726+
//
2727+
// const results = await csgoAPI.searchItems(query, limit);
2728+
//
2729+
// res.json({
2730+
// success: true,
2731+
// query,
2732+
// count: results.length,
2733+
// results
2734+
// });
2735+
// } catch (error) {
2736+
// winston.error('Item search error:', error);
2737+
// res.status(500).json({
2738+
// error: 'Failed to search items',
2739+
// message: error.message
2740+
// });
2741+
// }
2742+
// });
27432743

27442744
// Get case contents
27452745
app.get('/api/items/case/:caseName', async (req, res) => {
@@ -4347,6 +4347,91 @@ app.post('/api/portfolio/quick/price-check',
43474347

43484348
winston.info('Quick Actions API endpoints loaded');
43494349

4350+
// ============================================================================
4351+
// CS2 ITEM SEARCH API
4352+
// ============================================================================
4353+
4354+
const cs2ItemCache = require('./utils/cs2ItemCache');
4355+
4356+
/**
4357+
* Search CS2 items - Used for autocomplete and item selection
4358+
* GET /api/items/search?q=AK-47&limit=20
4359+
* GET /api/items/search (returns all items, limited by limit param)
4360+
*/
4361+
app.get('/api/items/search',
4362+
validateQuery(z.object({
4363+
q: z.string().optional().default(''),
4364+
limit: z.union([z.string().regex(/^\d+$/), z.number()]).transform(Number).optional().default(20)
4365+
})),
4366+
asyncHandler(async (req, res) => {
4367+
const { q: query, limit } = req.query;
4368+
4369+
const results = await cs2ItemCache.search(query, Math.min(limit, 100));
4370+
4371+
res.json({
4372+
success: true,
4373+
query,
4374+
count: results.length,
4375+
items: results
4376+
});
4377+
})
4378+
);
4379+
4380+
/**
4381+
* Get item by exact name
4382+
* GET /api/items/:itemName
4383+
*/
4384+
app.get('/api/items/:itemName',
4385+
asyncHandler(async (req, res) => {
4386+
const { itemName } = req.params;
4387+
4388+
const item = await cs2ItemCache.getItemByName(decodeURIComponent(itemName));
4389+
4390+
if (!item) {
4391+
throw new ApiError(404, 'Item not found', 'ITEM_NOT_FOUND');
4392+
}
4393+
4394+
res.json({
4395+
success: true,
4396+
item
4397+
});
4398+
})
4399+
);
4400+
4401+
/**
4402+
* Get cache statistics
4403+
* GET /api/items/cache/stats
4404+
*/
4405+
app.get('/api/items/cache/stats',
4406+
asyncHandler(async (req, res) => {
4407+
const stats = cs2ItemCache.getStats();
4408+
4409+
res.json({
4410+
success: true,
4411+
cache: stats
4412+
});
4413+
})
4414+
);
4415+
4416+
/**
4417+
* Force refresh cache (admin use)
4418+
* POST /api/items/cache/refresh
4419+
*/
4420+
app.post('/api/items/cache/refresh',
4421+
asyncHandler(async (req, res) => {
4422+
await cs2ItemCache.forceRefresh();
4423+
const stats = cs2ItemCache.getStats();
4424+
4425+
res.json({
4426+
success: true,
4427+
message: 'Cache refreshed successfully',
4428+
cache: stats
4429+
});
4430+
})
4431+
);
4432+
4433+
winston.info('CS2 Item Search API endpoints loaded');
4434+
43504435
// ============================================================================
43514436
// STEAM INVENTORY INTEGRATION
43524437
// ============================================================================
@@ -4528,6 +4613,17 @@ const http_server = require('http').Server(app);
45284613
http_server.listen(CONFIG.http.port);
45294614
winston.info('Listening for HTTP on port: ' + CONFIG.http.port);
45304615

4616+
// Initialize CS2 item cache
4617+
(async () => {
4618+
try {
4619+
await cs2ItemCache.initialize();
4620+
winston.info('CS2 item cache initialized successfully');
4621+
} catch (error) {
4622+
winston.error('Failed to initialize CS2 item cache:', error.message);
4623+
winston.warn('Item search functionality may be limited until cache is populated');
4624+
}
4625+
})();
4626+
45314627
queue.process(CONFIG.logins.length, botController, async (job) => {
45324628
const itemData = await botController.lookupFloat(job.data.link);
45334629
winston.debug(`Received itemData for ${job.data.link.getParams().a}`);

utils/cs2ItemCache.js

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/**
2+
* CS2 Item Cache Manager
3+
*
4+
* Downloads and caches CS2 item data from ByMykel's CSGO-API
5+
* Refreshes cache daily to ensure data is up-to-date
6+
*/
7+
8+
const fetch = require('node-fetch');
9+
const winston = require('winston');
10+
11+
class CS2ItemCache {
12+
constructor() {
13+
this.items = [];
14+
this.lastFetchTime = null;
15+
this.CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
16+
this.API_URL = 'https://raw.githubusercontent.com/ByMykel/CSGO-API/main/public/api/en/skins.json';
17+
this.isInitialized = false;
18+
this.isFetching = false;
19+
}
20+
21+
/**
22+
* Initialize the cache by fetching items on startup
23+
*/
24+
async initialize() {
25+
if (this.isInitialized) {
26+
return;
27+
}
28+
29+
winston.info('[CS2ItemCache] Initializing item cache...');
30+
await this.fetchItems();
31+
this.isInitialized = true;
32+
33+
// Set up automatic daily refresh
34+
this.scheduleRefresh();
35+
}
36+
37+
/**
38+
* Check if cache needs refresh (older than 24 hours)
39+
*/
40+
needsRefresh() {
41+
if (!this.lastFetchTime) {
42+
return true;
43+
}
44+
45+
const timeSinceLastFetch = Date.now() - this.lastFetchTime;
46+
return timeSinceLastFetch >= this.CACHE_DURATION;
47+
}
48+
49+
/**
50+
* Fetch items from ByMykel's API
51+
*/
52+
async fetchItems() {
53+
if (this.isFetching) {
54+
winston.debug('[CS2ItemCache] Fetch already in progress, skipping...');
55+
return this.items;
56+
}
57+
58+
try {
59+
this.isFetching = true;
60+
winston.info('[CS2ItemCache] Fetching CS2 items from API...');
61+
62+
const response = await fetch(this.API_URL, {
63+
timeout: 30000,
64+
headers: {
65+
'User-Agent': 'CSFloat-API/1.5.0'
66+
}
67+
});
68+
69+
if (!response.ok) {
70+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
71+
}
72+
73+
const data = await response.json();
74+
75+
if (data && Array.isArray(data)) {
76+
this.items = data;
77+
this.lastFetchTime = Date.now();
78+
winston.info(`[CS2ItemCache] Successfully cached ${this.items.length} items`);
79+
} else {
80+
winston.error('[CS2ItemCache] Invalid response format from API');
81+
}
82+
83+
return this.items;
84+
} catch (error) {
85+
winston.error('[CS2ItemCache] Failed to fetch items:', error.message);
86+
87+
// If we have cached items, continue using them
88+
if (this.items.length > 0) {
89+
winston.warn('[CS2ItemCache] Using stale cache due to fetch error');
90+
return this.items;
91+
}
92+
93+
throw error;
94+
} finally {
95+
this.isFetching = false;
96+
}
97+
}
98+
99+
/**
100+
* Get all cached items (refresh if needed)
101+
*/
102+
async getItems() {
103+
if (this.needsRefresh()) {
104+
await this.fetchItems();
105+
}
106+
107+
return this.items;
108+
}
109+
110+
/**
111+
* Search items by name
112+
* @param {string} query - Search query
113+
* @param {number} limit - Maximum results to return (default: 20)
114+
*/
115+
async search(query, limit = 20) {
116+
const items = await this.getItems();
117+
118+
if (!query || query.trim().length === 0) {
119+
return items.slice(0, limit);
120+
}
121+
122+
const searchTerm = query.toLowerCase().trim();
123+
124+
// Filter items by name match
125+
const results = items.filter(item => {
126+
const itemName = item.name.toLowerCase();
127+
return itemName.includes(searchTerm);
128+
});
129+
130+
// Sort by relevance (exact matches first, then starts with, then contains)
131+
results.sort((a, b) => {
132+
const aName = a.name.toLowerCase();
133+
const bName = b.name.toLowerCase();
134+
135+
// Exact match
136+
if (aName === searchTerm) return -1;
137+
if (bName === searchTerm) return 1;
138+
139+
// Starts with query
140+
if (aName.startsWith(searchTerm) && !bName.startsWith(searchTerm)) return -1;
141+
if (bName.startsWith(searchTerm) && !aName.startsWith(searchTerm)) return 1;
142+
143+
// Alphabetical
144+
return aName.localeCompare(bName);
145+
});
146+
147+
return results.slice(0, limit);
148+
}
149+
150+
/**
151+
* Get item by exact name
152+
* @param {string} name - Exact item name
153+
*/
154+
async getItemByName(name) {
155+
const items = await this.getItems();
156+
return items.find(item => item.name === name);
157+
}
158+
159+
/**
160+
* Get cache statistics
161+
*/
162+
getStats() {
163+
return {
164+
totalItems: this.items.length,
165+
lastFetchTime: this.lastFetchTime,
166+
cacheAge: this.lastFetchTime ? Date.now() - this.lastFetchTime : null,
167+
needsRefresh: this.needsRefresh(),
168+
nextRefresh: this.lastFetchTime
169+
? new Date(this.lastFetchTime + this.CACHE_DURATION).toISOString()
170+
: null
171+
};
172+
}
173+
174+
/**
175+
* Schedule automatic daily refresh
176+
*/
177+
scheduleRefresh() {
178+
// Check every hour if refresh is needed
179+
setInterval(async () => {
180+
if (this.needsRefresh()) {
181+
winston.info('[CS2ItemCache] Cache expired, refreshing...');
182+
await this.fetchItems();
183+
}
184+
}, 60 * 60 * 1000); // Check every hour
185+
186+
winston.info('[CS2ItemCache] Scheduled automatic daily refresh');
187+
}
188+
189+
/**
190+
* Force refresh the cache
191+
*/
192+
async forceRefresh() {
193+
winston.info('[CS2ItemCache] Forcing cache refresh...');
194+
return await this.fetchItems();
195+
}
196+
}
197+
198+
// Export singleton instance
199+
module.exports = new CS2ItemCache();

0 commit comments

Comments
 (0)