retropulse/backend/retroClient.js
2025-09-02 22:27:25 +05:30

219 lines
6.9 KiB
JavaScript

// 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();