// backend/retroClient.js 'use strict'; const { buildAuthorization, getUserProfile, getUserRecentAchievements, getGame } = require('@retroachievements/api'); class RetroClient { constructor() { const RA_USER = process.env.RA_USER; const RA_KEY = process.env.RA_KEY; if (!RA_USER || !RA_KEY) { throw new Error('RetroAchievements username and API key must be set in .env file'); } this.authorization = buildAuthorization({ username: RA_USER, webApiKey: RA_KEY }); console.log('🎮 RetroAchievements API client initialized'); } // Helper method to build correct image URLs buildImageUrl(imagePath, baseUrl) { if (!imagePath) return null; const toStr = (v) => (typeof v === 'string' ? v : String(v)); // If full URL provided, return as-is if (/^https?:\/\//i.test(imagePath)) { return imagePath; } const pathStr = toStr(imagePath); // Handle full server-relative paths that start with / if (pathStr.startsWith('/')) { // Ensure .png for badges if (pathStr.startsWith('/Badge/')) { const name = pathStr.split('/').pop().replace(/\.(png|jpe?g|gif|webp)$/i, ''); return `https://media.retroachievements.org/Badge/${name}.png`; } return `https://media.retroachievements.org${pathStr}`; } // Normalize base URL (strip trailing slashes) const normalizedBase = toStr(baseUrl || '').replace(/\/+$/, ''); // Special handling for badges - always ensure .png extension const isBadgeContext = /\/Badge(\/|$)/i.test(normalizedBase); if (isBadgeContext) { const cleanImagePath = pathStr.replace(/\.(png|jpe?g|gif|webp)$/i, ''); return `${normalizedBase}/${cleanImagePath}.png`; } // For other images (UserPic, Images), build normally return `${normalizedBase}/${pathStr}`; } async getUserProfile(username) { try { console.log(`📊 Fetching profile for user: ${username}`); const profile = await getUserProfile(this.authorization, { username }); return profile; } catch (error) { console.error(`❌ Error fetching user profile for ${username}:`, error.message); throw new Error(`Failed to fetch user profile: ${error.message}`); } } async getUserRecentAchievements(username, count = 10) { try { console.log(`🏆 Fetching recent achievements for user: ${username}`); const achievements = await getUserRecentAchievements(this.authorization, { username, count }); return achievements; } catch (error) { console.error(`❌ Error fetching recent achievements for ${username}:`, error.message); throw new Error(`Failed to fetch recent achievements: ${error.message}`); } } async getGameInfo(gameId) { try { console.log(`🎮 Fetching game info for ID: ${gameId}`); const game = await getGame(this.authorization, { gameId }); return game; } catch (error) { console.error(`❌ Error fetching game info for ID ${gameId}:`, error.message); throw new Error(`Failed to fetch game info: ${error.message}`); } } async getUserData(username) { try { const profile = await this.getUserProfile(username); let lastGame = null; let lastAchievement = null; if (profile.lastGameId) { try { lastGame = await this.getGameInfo(profile.lastGameId); } catch (gameError) { console.warn(`⚠️ Could not fetch last game info: ${gameError.message}`); } } try { const recentAchievements = await this.getUserRecentAchievements(username, 1); if (recentAchievements && recentAchievements.length > 0) { lastAchievement = recentAchievements; } } catch (achievementError) { console.warn(`⚠️ Could not fetch recent achievements: ${achievementError.message}`); } return { user: { username: profile.user, displayName: profile.user, avatar: this.buildImageUrl( profile.userPic, 'https://media.retroachievements.org/UserPic' ), motto: profile.motto || '', totalPoints: profile.totalPoints || 0, totalTruePoints: profile.totalTruePoints || 0, memberSince: profile.memberSince || null, rank: profile.rank || null }, lastGame: lastGame ? { id: lastGame.gameId, title: lastGame.title, icon: this.buildImageUrl( lastGame.gameIcon, 'https://media.retroachievements.org/Images' ), consoleName: lastGame.consoleName } : null, lastAchievement: lastAchievement ? { id: lastAchievement.achievementId, title: lastAchievement.title, description: lastAchievement.description, points: lastAchievement.points, trueRatio: lastAchievement.trueRatio, gameTitle: lastAchievement.gameTitle, gameIcon: this.buildImageUrl( lastAchievement.gameIcon, 'https://media.retroachievements.org/Images' ), badgeName: this.buildImageUrl( lastAchievement.badgeName, 'https://media.retroachievements.org/Badge' ), dateEarned: lastAchievement.dateEarned } : null }; } catch (error) { console.error(`❌ Error in getUserData for ${username}:`, error.message); throw error; } } async getOverlayData(username, count = 5) { try { const recentAchievements = await this.getUserRecentAchievements(username, count); const formattedAchievements = recentAchievements.map((achievement) => { // Debug log to see what we're getting from the API console.log( `🔍 Processing badge for achievement "${achievement.title}": badgeName="${achievement.badgeName}"` ); return { id: achievement.achievementId, title: achievement.title, description: achievement.description, points: achievement.points, trueRatio: achievement.trueRatio || achievement.points, gameTitle: achievement.gameTitle, gameIcon: this.buildImageUrl( achievement.gameIcon, 'https://media.retroachievements.org/Images' ), badgeName: this.buildImageUrl( achievement.badgeName, 'https://media.retroachievements.org/Badge' ), dateEarned: achievement.dateEarned }; }); return { username, recentAchievements: formattedAchievements, lastUpdated: new Date().toISOString() }; } catch (error) { console.error(`❌ Error in getOverlayData for ${username}:`, error.message); throw error; } } } module.exports = new RetroClient();