retropulse/backend/retroClient.js
2025-09-03 21:55:40 +05:30

226 lines
9.6 KiB
JavaScript

'use strict';
const {
buildAuthorization,
getUserProfile,
getUserRecentAchievements,
getGame,
getGameInfoAndUserProgress
} = 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[0];
}
} 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);
let gameProgress = null;
let gameId = null;
// First, try to get gameId from the most recent achievement. This is the most "live" data.
if (recentAchievements && recentAchievements.length > 0) {
gameId = recentAchievements[0].gameId;
} else {
// If no recent achievements, fall back to the user's profile to find their last played game.
console.log(`No recent achievements for ${username}. Checking profile for last game.`);
try {
const profile = await this.getUserProfile(username);
if (profile.lastGameId) {
gameId = profile.lastGameId;
}
} catch (profileError) {
console.warn(`Could not fetch profile to find last game: ${profileError.message}`);
}
}
// If we found a gameId from either method, fetch its progress.
if (gameId) {
try {
const progressData = await getGameInfoAndUserProgress(this.authorization, {
username,
gameId
});
gameProgress = {
title: progressData.title,
icon: this.buildImageUrl(progressData.imageIcon, 'https://media.retroachievements.org/Images'),
consoleName: progressData.consoleName,
numAwarded: progressData.numAwardedToUser,
totalAchievements: progressData.numAchievements,
completion: progressData.userCompletion,
};
} catch (progressError) {
console.warn(`Could not fetch game progress for gameId ${gameId}: ${progressError.message}`);
}
}
const formattedAchievements = recentAchievements.map((achievement) => {
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,
gameProgress,
recentAchievements: formattedAchievements,
lastUpdated: new Date().toISOString()
};
} catch (error) {
console.error(`Error in getOverlayData for ${username}:`, error.message);
throw error;
}
}
}
module.exports = new RetroClient();