@@ -172,15 +172,26 @@ router.get("/", cacheMiddleware(30, playersKeyGenerator), async (req, res) => {
172172 const sortField = validSortFields . includes ( sort ) ? sort : "total_playtime" ;
173173 const sortOrder = order === "asc" ? "ASC" : "DESC" ;
174174
175- // Get all player data grouped by steamid and game
175+ // Optimized: Use SQL aggregation with JSON functions for better performance
176176 let query = `
177177 SELECT
178- steamid,
179- latest_name as name,
180- game,
181- SUM(playtime) as total_playtime,
182- MAX(last_seen) as last_seen,
183- MAX(avatar) as avatar
178+ steamid,
179+ MAX(latest_name) as name,
180+ MAX(avatar) as avatar,
181+ MAX(CASE WHEN game = 'csgo' THEN
182+ JSON_OBJECT(
183+ 'total_playtime', SUM(playtime),
184+ 'last_seen', MAX(last_seen)
185+ )
186+ END) as csgo,
187+ MAX(CASE WHEN game = 'counterstrike2' THEN
188+ JSON_OBJECT(
189+ 'total_playtime', SUM(playtime),
190+ 'last_seen', MAX(last_seen)
191+ )
192+ END) as counterstrike2,
193+ SUM(playtime) as _total_playtime,
194+ MAX(last_seen) as _last_seen
184195 FROM players
185196 WHERE 1=1
186197 ` ;
@@ -196,99 +207,55 @@ router.get("/", cacheMiddleware(30, playersKeyGenerator), async (req, res) => {
196207 params . push ( `%${ sanitizeString ( name , 100 ) } %` ) ;
197208 }
198209
199- query += " GROUP BY steamid, game " ;
210+ query += " GROUP BY steamid" ;
200211
201- const [ rawPlayers ] = await pool . query ( query , params ) ;
202-
203- // Group by steamid and structure data by game
204- const playerMap = new Map ( ) ;
205-
206- for ( const row of rawPlayers ) {
207- if ( ! playerMap . has ( row . steamid ) ) {
208- playerMap . set ( row . steamid , {
209- steamid : row . steamid ,
210- name : row . name , // Will be updated to most recent
211- avatar : row . avatar ,
212- csgo : { } ,
213- counterstrike2 : { } ,
214- _lastSeen : null , // For sorting
215- _totalPlaytime : 0 , // For sorting
216- } ) ;
217- }
218-
219- const player = playerMap . get ( row . steamid ) ;
220-
221- // Update name to most recent across all games
222- if (
223- ! player . _lastSeen ||
224- new Date ( row . last_seen ) > new Date ( player . _lastSeen )
225- ) {
226- player . name = row . name ;
227- player . _lastSeen = row . last_seen ;
228- }
229-
230- // Add game-specific stats
231- if ( row . game === "csgo" ) {
232- player . csgo = {
233- total_playtime : parseInt ( row . total_playtime , 10 ) || 0 ,
234- last_seen : row . last_seen ,
235- } ;
236- } else if ( row . game === "counterstrike2" ) {
237- player . counterstrike2 = {
238- total_playtime : parseInt ( row . total_playtime , 10 ) || 0 ,
239- last_seen : row . last_seen ,
240- } ;
241- }
242-
243- // Track combined playtime for sorting
244- player . _totalPlaytime += parseInt ( row . total_playtime , 10 ) || 0 ;
212+ // Add SQL-based sorting instead of JavaScript sorting
213+ if ( sortField === "total_playtime" ) {
214+ query += ` ORDER BY _total_playtime ${ sortOrder } ` ;
215+ } else if ( sortField === "last_seen" ) {
216+ query += ` ORDER BY _last_seen ${ sortOrder } ` ;
217+ } else {
218+ query += ` ORDER BY steamid ${ sortOrder } ` ;
245219 }
246220
247- // Convert map to array and remove internal sorting fields
248- let players = Array . from ( playerMap . values ( ) ) . map ( ( p ) => {
249- const { _lastSeen, _totalPlaytime, ...playerData } = p ;
250- return playerData ;
251- } ) ;
221+ // Add pagination in SQL
222+ query += " LIMIT ? OFFSET ?" ;
223+ params . push ( validLimit , offset ) ;
252224
253- // Sort based on requested field
254- players . sort ( ( a , b ) => {
255- let aVal , bVal ;
256-
257- if ( sortField === "total_playtime" ) {
258- // Sum playtime across both games for sorting
259- aVal =
260- ( a . csgo . total_playtime || 0 ) + ( a . counterstrike2 . total_playtime || 0 ) ;
261- bVal =
262- ( b . csgo . total_playtime || 0 ) + ( b . counterstrike2 . total_playtime || 0 ) ;
263- } else if ( sortField === "last_seen" ) {
264- // Get most recent last_seen across both games
265- const aDate =
266- [ a . csgo . last_seen , a . counterstrike2 . last_seen ]
267- . filter ( ( d ) => d )
268- . sort ( )
269- . reverse ( ) [ 0 ] || "" ;
270- const bDate =
271- [ b . csgo . last_seen , b . counterstrike2 . last_seen ]
272- . filter ( ( d ) => d )
273- . sort ( )
274- . reverse ( ) [ 0 ] || "" ;
275- aVal = aDate ;
276- bVal = bDate ;
277- } else {
278- aVal = a [ sortField ] ;
279- bVal = b [ sortField ] ;
280- }
225+ const [ rawPlayers ] = await pool . query ( query , params ) ;
281226
282- if ( sortOrder === "DESC" ) {
283- return aVal > bVal ? - 1 : aVal < bVal ? 1 : 0 ;
284- } else {
285- return aVal < bVal ? - 1 : aVal > bVal ? 1 : 0 ;
286- }
227+ // Parse JSON fields from SQL (MariaDB returns JSON as strings)
228+ const players = rawPlayers . map ( ( row ) => {
229+ const { _total_playtime, _last_seen, ...player } = row ;
230+
231+ // Parse JSON objects or set to empty objects
232+ player . csgo = row . csgo
233+ ? typeof row . csgo === "string"
234+ ? JSON . parse ( row . csgo )
235+ : row . csgo
236+ : { } ;
237+ player . counterstrike2 = row . counterstrike2
238+ ? typeof row . counterstrike2 === "string"
239+ ? JSON . parse ( row . counterstrike2 )
240+ : row . counterstrike2
241+ : { } ;
242+
243+ return player ;
287244 } ) ;
288245
289- // Apply pagination
290- const total = players . length ;
291- players = players . slice ( offset , offset + validLimit ) ;
246+ // Get total count (separate query for accuracy)
247+ let countQuery =
248+ "SELECT COUNT(DISTINCT steamid) as total FROM players WHERE 1=1" ;
249+ const countParams = [ ] ;
250+ if ( game ) {
251+ countQuery += " AND game = ?" ;
252+ countParams . push ( sanitizeString ( game , 50 ) ) ;
253+ }
254+ if ( name ) {
255+ countQuery += " AND latest_name LIKE ?" ;
256+ countParams . push ( `%${ sanitizeString ( name , 100 ) } %` ) ;
257+ }
258+ const [ [ { total } ] ] = await pool . query ( countQuery , countParams ) ;
292259
293260 res . json ( {
294261 data : players ,
0 commit comments