import { getDiscordAppInfo } from "@/lib/game/discord-app"; import { getDetectables, steamIdFromEntry } from "@/lib/game/detectables"; import { getSteamDetails, getSteamReviewSummary } from "@/lib/game/steam"; import { getIgdbGame, igdbImage } from "@/lib/game/igdb"; import { getItadId, itadFetch } from "@/lib/game/itad"; import { toIsoDate } from "@/lib/game/game-format"; import type { GameExternalLink, GamePricePoint, GameProfile, IgdbGame, ItadGameInfo, ItadOverviewPrice, ItadReview, SteamAppDetails, } from "@/lib/game/game-profile-types"; import type { DetectableEntry } from "@/lib/game/game-profile-types"; const DISTRIBUTOR_PLATFORM: Partial> = { xbox: "Xbox", xbox_title: "Xbox", xbox_game_pass: "Xbox", playstation: "PlayStation", nintendo: "Switch", }; const IGDB_WEBSITE_LABELS: Record = { 1: "Website", 4: "Facebook", 5: "X", 6: "Twitch", 8: "Instagram", 9: "YouTube", 13: "Steam", 14: "Reddit", 16: "Epic", 17: "GOG", 18: "Discord", }; type SteamData = SteamAppDetails["data"]; export function mergePeople( igdb: IgdbGame | null, steam: SteamData | null, itadInfo: ItadGameInfo | null, ): { developers: string[]; publishers: string[] } { const companies = igdb?.involved_companies ?? []; if (companies.length === 0) { const steamDevs = steam?.developers?.length ? steam.developers : itadInfo?.developers?.map((d) => d.name).filter((n): n is string => !!n) || []; const steamPubs = steam?.publishers?.length ? steam.publishers : itadInfo?.publishers?.map((d) => d.name).filter((n): n is string => !!n) || []; return { developers: steamDevs, publishers: steamPubs }; } const names = (developer: boolean): string[] => [ ...new Set( companies .filter((c) => (developer ? c.developer : c.publisher)) .map((c) => c.company?.name) .filter((n): n is string => !!n), ), ]; return { developers: names(true), publishers: names(false) }; } export function mergePlatforms( igdb: IgdbGame | null, steam: SteamData | null, entry: DetectableEntry | undefined, ): string[] { const platforms = new Set(); for (const p of igdb?.platforms ?? []) { const raw = (p.name ?? "").trim(); if (!raw) { continue; } if (raw === "PC (Microsoft Windows)") { platforms.add("Windows"); } else if (raw === "Mac") { platforms.add("MacOS"); } else { platforms.add(raw); } } if (steam?.platforms) { if (steam.platforms.windows) { platforms.add("Windows"); } if (steam.platforms.mac) { platforms.add("MacOS"); } if (steam.platforms.linux) { platforms.add("Linux"); } } for (const sku of entry?.third_party_skus ?? []) { const mapped = DISTRIBUTOR_PLATFORM[sku.distributor]; if (mapped) { platforms.add(mapped); } } return [...platforms]; } export function buildLinks(igdb: IgdbGame | null): GameExternalLink[] { const links: GameExternalLink[] = []; for (const site of igdb?.websites ?? []) { if (!site.url) { continue; } const url = site.url.startsWith("http") ? site.url : `https://${site.url}`; let parsed: URL | null = null; try { parsed = new URL(url); } catch { continue; } if (parsed.protocol !== "https:") { continue; } let label = site.category ? (IGDB_WEBSITE_LABELS[site.category] ?? null) : null; if (!label) { label = parsed.hostname.replace(/^www\./, ""); } if (!links.some((l) => l.url === url)) { links.push({ label, url }); } } return links; } export function selectScreenshots(igdb: IgdbGame | null, steam: SteamData | null): string[] { return [ ...(igdb?.screenshots ?? []) .map((s) => igdbImage(s.url, "t_screenshot_big")) .filter((u): u is string => !!u), ...(steam?.screenshots ?? []).map((s) => s.path_full).filter((u): u is string => !!u), ].slice(0, 8); } export function buildGenres(igdb: IgdbGame | null, itadInfo: ItadGameInfo | null): string[] { return [ ...(igdb?.genres ?? []).map((g) => g.name).filter((n): n is string => !!n), ...(igdb?.genres?.some((g) => !!g.name) ? [] : (itadInfo?.tags ?? []).slice(0, 5)), ].slice(0, 6); } export function selectDescription( lang: string, igdb: IgdbGame | null, steam: SteamData | null, ): string | null { return lang === "en" ? (igdb?.summary ?? steam?.short_description ?? null) : (steam?.short_description ?? igdb?.summary ?? null); } export function selectArtwork( igdb: IgdbGame | null, steam: SteamData | null, itadInfo: ItadGameInfo | null, ): string | null { return ( igdbImage(igdb?.cover?.url, "t_cover_big") ?? steam?.header_image ?? itadInfo?.assets?.banner600 ?? null ); } export function toPricePoint(p: ItadOverviewPrice["current"]): GamePricePoint | null { if (!p?.price || typeof p.price.amount !== "number") { return null; } return { price: p.price.amount, regular: p.regular?.amount ?? p.price.amount, currency: p.price.currency ?? "", shop: p.shop?.name ?? "", url: p.url?.startsWith("https://") ? p.url : "", }; } export function mapItadBlock( itadInfo: ItadGameInfo | null, priceEntry: { current?: ItadOverviewPrice["current"]; lowest?: ItadOverviewPrice["current"] } | undefined, ): NonNullable | null { if (!process.env.ITAD_API_KEY || (!itadInfo && !priceEntry)) { return null; } return { prices: toPricePoint(priceEntry?.current), historyLow: toPricePoint(priceEntry?.lowest), pageUrl: itadInfo?.urls?.game ?? null, playerCount: itadInfo?.players?.recent ?? null, reviews: (itadInfo?.reviews ?? []) .filter( (r): r is ItadReview & { source: string; url: string } => !!r.source && !!r.url, ) .map((r) => ({ score: r.score ?? null, source: r.source, count: r.count ?? null, url: r.url, })), }; } export async function getGameProfile( applicationId: string, lang = "en", country = "TR", ): Promise { if (!/^\d+$/.test(applicationId)) { return null; } const appInfo = await getDiscordAppInfo(applicationId); if (!appInfo) { return null; } const detectables = await getDetectables(); const entry = detectables.find((e) => e.id === appInfo.gameId || e.id === applicationId); const steamId = steamIdFromEntry(entry); const [steam, steamReviews, igdb] = await Promise.all([ steamId ? getSteamDetails(steamId, lang, country) : Promise.resolve(null), steamId ? getSteamReviewSummary(steamId) : Promise.resolve(null), getIgdbGame(appInfo.name || entry?.name || "", steamId), ]); const itadId = process.env.ITAD_API_KEY ? await getItadId(igdb?.name ?? steam?.name ?? appInfo.name) : null; const [itadInfo, itadOverview] = itadId ? await Promise.all([ itadFetch(`/games/info/v2?id=${itadId}`), itadFetch<{ prices?: ItadOverviewPrice[] }>(`/games/overview/v2?country=${country}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([itadId]), }), ]) : [null, null]; const { developers, publishers } = mergePeople(igdb, steam, itadInfo); const platforms = mergePlatforms(igdb, steam, entry); const links = buildLinks(igdb); const screenshots = selectScreenshots(igdb, steam); const priceEntry = itadOverview?.prices?.[0]; return { applicationId, gameId: appInfo.gameId, gameUrl: appInfo.gameUrl, name: igdb?.name ?? steam?.name ?? appInfo.name, genres: buildGenres(igdb, itadInfo), description: selectDescription(lang, igdb, steam), developers, publishers, releaseDate: toIsoDate(igdb?.first_release_date) ?? itadInfo?.releaseDate ?? steam?.release_date?.date ?? null, platforms, links, coverUrl: igdbImage(igdb?.cover?.url, "t_cover_big"), artworkUrl: selectArtwork(igdb, steam, itadInfo), screenshots, steamId, steamUrl: steamId ? `https://store.steampowered.com/app/${steamId}/` : null, igdbUrl: igdb?.slug ? `https://www.igdb.com/games/${igdb.slug}` : null, steamReviews, itad: mapItadBlock(itadInfo, priceEntry), sources: { igdb: !!igdb, steam: !!steam, itad: !!(itadInfo || priceEntry), }, }; }