// ────────────────────────────────────────────────────────────────────────────── // GOG (Good Old Games) API service // Uses GOG OAuth2 for user authentication. All paginated requests include a // 500 ms pause between pages to respect rate-limits. // ────────────────────────────────────────────────────────────────────────────── const GOG_CLIENT_ID = '46899977096215655'; const GOG_CLIENT_SECRET = '9d85c43b1482497dbbce61f6e4aa173a433796eebd2c1f0f7f015c4c2e57571'; const GOG_AUTH_URL = 'https://auth.gog.com/auth'; const GOG_TOKEN_URL = 'https://auth.gog.com/token'; const GOG_EMBED_URL = 'https://embed.gog.com'; // ── Types ──────────────────────────────────────────────────────────────────── export interface GogTokens { accessToken: string; refreshToken: string; expiresAt: number; // unix-ms userId: string; } export interface GogUserInfo { userId: string; username: string; avatarUrl: string; } export interface GogGame { gogId: number; title: string; image: string; // cover URL from GOG slug: string; } // ── OAuth helpers ──────────────────────────────────────────────────────────── /** * Build the GOG OAuth authorization URL that the user should visit to grant * access. After approval, GOG will redirect to `redirectUri` with a `code` * query parameter. */ export function getGogAuthUrl(redirectUri: string): string { const params = new URLSearchParams({ client_id: GOG_CLIENT_ID, redirect_uri: redirectUri, response_type: 'code', layout: 'client2', }); return `${GOG_AUTH_URL}?${params.toString()}`; } /** * Exchange an authorization code for GOG access + refresh tokens. */ export async function exchangeGogCode( code: string, redirectUri: string, ): Promise { console.log('[GOG] Exchanging authorization code for tokens...'); const params = new URLSearchParams({ client_id: GOG_CLIENT_ID, client_secret: GOG_CLIENT_SECRET, grant_type: 'authorization_code', code, redirect_uri: redirectUri, }); const res = await fetch(`${GOG_TOKEN_URL}?${params.toString()}`); if (!res.ok) { const text = await res.text(); console.error(`[GOG] Token exchange failed (${res.status}): ${text}`); throw new Error(`[GOG] Token exchange failed (${res.status}): ${text}`); } const data = (await res.json()) as { access_token: string; refresh_token: string; expires_in: number; user_id: string; token_type: string; }; const tokens: GogTokens = { accessToken: data.access_token, refreshToken: data.refresh_token, expiresAt: Date.now() + data.expires_in * 1000, userId: data.user_id, }; console.log( `[GOG] Tokens acquired for user ${tokens.userId}, expires in ${Math.round(data.expires_in / 3600)} hours`, ); return tokens; } /** * Obtain a fresh set of tokens using an existing refresh token. */ export async function refreshGogToken( refreshToken: string, ): Promise { console.log('[GOG] Refreshing access token...'); const params = new URLSearchParams({ client_id: GOG_CLIENT_ID, client_secret: GOG_CLIENT_SECRET, grant_type: 'refresh_token', refresh_token: refreshToken, }); const res = await fetch(`${GOG_TOKEN_URL}?${params.toString()}`); if (!res.ok) { const text = await res.text(); console.error(`[GOG] Token refresh failed (${res.status}): ${text}`); throw new Error(`[GOG] Token refresh failed (${res.status}): ${text}`); } const data = (await res.json()) as { access_token: string; refresh_token: string; expires_in: number; user_id: string; token_type: string; }; const tokens: GogTokens = { accessToken: data.access_token, refreshToken: data.refresh_token, expiresAt: Date.now() + data.expires_in * 1000, userId: data.user_id, }; console.log( `[GOG] Token refreshed for user ${tokens.userId}, expires in ${Math.round(data.expires_in / 3600)} hours`, ); return tokens; } // ── User info ──────────────────────────────────────────────────────────────── /** * Fetch basic user profile information from the GOG embed API. */ export async function fetchGogUserInfo( accessToken: string, ): Promise { console.log('[GOG] Fetching user info...'); const res = await fetch(`${GOG_EMBED_URL}/userData.json`, { headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!res.ok) { const text = await res.text(); console.error(`[GOG] User info request failed (${res.status}): ${text}`); throw new Error(`[GOG] User info request failed (${res.status}): ${text}`); } const data = (await res.json()) as { userId: string; username: string; galaxyUserId: string; avatar: string; }; const userInfo: GogUserInfo = { userId: data.galaxyUserId || data.userId, username: data.username, avatarUrl: data.avatar ? `https:${data.avatar}` : '', }; console.log(`[GOG] User info fetched: ${userInfo.username}`); return userInfo; } // ── Game library ───────────────────────────────────────────────────────────── /** * Fetch the complete list of owned GOG games for the authenticated user. * * The GOG embed API paginates results (one page at a time). We iterate * through all pages with a 500 ms pause between requests to stay friendly. */ export async function fetchGogGames( accessToken: string, ): Promise { console.log('[GOG] Fetching game library...'); const allGames: GogGame[] = []; let currentPage = 1; let totalPages = 1; do { const url = `${GOG_EMBED_URL}/account/getFilteredProducts?mediaType=1&page=${currentPage}`; const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!res.ok) { const text = await res.text(); console.error( `[GOG] Game library request failed on page ${currentPage} (${res.status}): ${text}`, ); throw new Error( `[GOG] Game library request failed (${res.status}): ${text}`, ); } const data = (await res.json()) as { totalPages: number; products: Array<{ id: number; title: string; image: string; slug: string; }>; }; totalPages = data.totalPages; for (const product of data.products) { allGames.push({ gogId: product.id, title: product.title, image: product.image ? `https:${product.image}` : '', slug: product.slug, }); } console.log( `[GOG] Page ${currentPage}/${totalPages} fetched (${data.products.length} games)`, ); currentPage++; // Rate-limit: 500 ms between page requests if (currentPage <= totalPages) { await new Promise((resolve) => setTimeout(resolve, 500)); } } while (currentPage <= totalPages); console.log(`[GOG] Library complete: ${allGames.length} games total`); return allGames; } // ── Cover URL helper ───────────────────────────────────────────────────────── /** * Convert a GOG image base path into a full HTTPS cover URL. * * GOG images are returned as protocol-relative paths like * `//images-1.gog.com/xxx`. This helper ensures HTTPS and optionally * appends a size suffix (e.g. `_196.jpg`). * * @param imageBase The raw image path returned by the GOG API * @param size Optional size suffix to append (e.g. `_196.jpg`) */ export function gogCoverUrl(imageBase: string, size?: string): string { let url = imageBase; // Ensure HTTPS prefix if (url.startsWith('//')) { url = `https:${url}`; } else if (!url.startsWith('http')) { url = `https://${url}`; } // Append size suffix if provided if (size) { url = `${url}${size}`; } return url; }