IGDB-Integration für Game Library + Electron Update-Button im Version-Modal

- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-08 01:48:15 +01:00
parent aec1142bff
commit b404c20eca
6 changed files with 785 additions and 25 deletions

View file

@ -3,6 +3,7 @@ import type { Plugin, PluginContext } from '../../core/plugin.js';
import { sseBroadcast } from '../../core/sse.js';
import fs from 'node:fs';
import path from 'node:path';
import { lookupGame, enrichGames, type IgdbGameInfo } from './igdb.js';
// ── Types ──
@ -11,6 +12,7 @@ interface SteamGame {
name: string;
playtime_forever: number; // minutes
img_icon_url: string;
igdb?: IgdbGameInfo;
}
interface SteamUser {
@ -185,6 +187,21 @@ const gameLibraryPlugin: Plugin = {
saveData(ctx, data);
broadcastUpdate(data);
// Fire-and-forget IGDB enrichment
enrichGames(games.map(g => ({ appid: g.appid, name: g.name }))).then(igdbMap => {
const data2 = loadData(ctx);
const user2 = data2.users[steamId];
if (user2) {
let count = 0;
for (const game of user2.games) {
const info = igdbMap.get(game.appid);
if (info) { game.igdb = info; count++; }
}
saveData(ctx, data2);
console.log(`[GameLibrary] IGDB auto-enrichment: ${count}/${games.length} games`);
}
}).catch(err => console.error('[GameLibrary] IGDB auto-enrichment error:', err));
const personaName = profile.personaName;
console.log(`[GameLibrary] Steam-Benutzer verknuepft: ${personaName} (${steamId}) - ${games.length} Spiele`);
@ -283,6 +300,7 @@ const gameLibraryPlugin: Plugin = {
appid: refGame.appid,
name: refGame.name,
img_icon_url: refGame.img_icon_url,
...(refGame.igdb ? { igdb: refGame.igdb } : {}),
owners,
};
});
@ -330,6 +348,89 @@ const gameLibraryPlugin: Plugin = {
res.json({ games, count: games.length });
});
// ── GET /api/game-library/igdb/enrich/:steamId ──
app.get('/api/game-library/igdb/enrich/:steamId', async (req, res) => {
try {
const { steamId } = req.params;
const force = req.query.force === 'true';
const data = loadData(ctx);
const user = data.users[steamId];
if (!user) {
res.status(404).json({ error: 'Benutzer nicht gefunden.' });
return;
}
const gamesToEnrich = force
? user.games.map(g => ({ appid: g.appid, name: g.name }))
: user.games.filter(g => !g.igdb).map(g => ({ appid: g.appid, name: g.name }));
const igdbMap = await enrichGames(gamesToEnrich);
// Reload data to avoid stale writes
const freshData = loadData(ctx);
const freshUser = freshData.users[steamId];
if (!freshUser) {
res.status(404).json({ error: 'Benutzer nicht gefunden.' });
return;
}
let enriched = 0;
for (const game of freshUser.games) {
const info = igdbMap.get(game.appid);
if (info) {
game.igdb = info;
enriched++;
}
}
saveData(ctx, freshData);
console.log(`[GameLibrary] IGDB enrichment for ${freshUser.personaName}: ${enriched}/${freshUser.games.length} games`);
res.json({ enriched, total: freshUser.games.length });
} catch (err) {
console.error('[GameLibrary] IGDB enrich error:', err);
res.status(500).json({ error: 'Fehler bei der IGDB-Anreicherung.' });
}
});
// ── GET /api/game-library/igdb/game/:appid ──
app.get('/api/game-library/igdb/game/:appid', async (req, res) => {
try {
const appid = parseInt(req.params.appid, 10);
if (isNaN(appid)) {
res.status(400).json({ error: 'Ungueltige App-ID.' });
return;
}
// Find game name from any user's library
const data = loadData(ctx);
let gameName = '';
for (const user of Object.values(data.users)) {
const game = user.games.find(g => g.appid === appid);
if (game) {
gameName = game.name;
break;
}
}
if (!gameName) {
res.status(404).json({ error: 'Spiel nicht in der Bibliothek gefunden.' });
return;
}
const info = await lookupGame(appid, gameName);
if (!info) {
res.status(404).json({ error: 'Spiel nicht in IGDB gefunden.' });
return;
}
res.json(info);
} catch (err) {
console.error('[GameLibrary] IGDB game lookup error:', err);
res.status(500).json({ error: 'Fehler bei der IGDB-Abfrage.' });
}
});
// ── POST /api/game-library/refresh/:steamId ──
app.post('/api/game-library/refresh/:steamId', async (req, res) => {
try {
@ -358,6 +459,21 @@ const gameLibraryPlugin: Plugin = {
saveData(ctx, data);
broadcastUpdate(data);
// Fire-and-forget IGDB enrichment
enrichGames(games.map(g => ({ appid: g.appid, name: g.name }))).then(igdbMap => {
const data2 = loadData(ctx);
const user2 = data2.users[steamId];
if (user2) {
let count = 0;
for (const game of user2.games) {
const info = igdbMap.get(game.appid);
if (info) { game.igdb = info; count++; }
}
saveData(ctx, data2);
console.log(`[GameLibrary] IGDB auto-enrichment: ${count}/${games.length} games`);
}
}).catch(err => console.error('[GameLibrary] IGDB auto-enrichment error:', err));
console.log(`[GameLibrary] Aktualisiert: ${profile.personaName} (${steamId}) - ${games.length} Spiele`);
res.json({