refactor: game-stub route now uses ensureSteamGame pipeline
This commit is contained in:
+13
-111
@@ -1,127 +1,29 @@
|
|||||||
import { Elysia, t } from "elysia"
|
import { Elysia, t } from "elysia"
|
||||||
import { db } from "@/lib/db/index"
|
import { ensureSteamGame } from "@/lib/steam/sync"
|
||||||
import { games } from "@/lib/db/schema"
|
|
||||||
import { eq } from "drizzle-orm"
|
|
||||||
import { validateImageUrl, fetchSteamGridDBCover } from "@/lib/steam/sync"
|
|
||||||
|
|
||||||
interface SteamAppDetails {
|
|
||||||
type?: string
|
|
||||||
steam_appid: number
|
|
||||||
name: string
|
|
||||||
developers?: string[]
|
|
||||||
publishers?: string[]
|
|
||||||
header_image?: string
|
|
||||||
genres?: { id: string; description: string }[]
|
|
||||||
website?: string
|
|
||||||
pc_requirements?: { minimum?: string; recommended?: string }
|
|
||||||
metacritic?: { score: number; url: string }
|
|
||||||
recommendations?: { total: number }
|
|
||||||
price_overview?: { currency: string; initial: number; final: number }
|
|
||||||
is_free?: boolean
|
|
||||||
release_date?: { coming_soon: boolean; date: string }
|
|
||||||
categories?: { id: string; description: string }[]
|
|
||||||
platforms?: { windows: boolean; mac: boolean; linux: boolean }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const gameStubRoutes = new Elysia({ prefix: "/games" }).post(
|
export const gameStubRoutes = new Elysia({ prefix: "/games" }).post(
|
||||||
"/stub",
|
"/stub",
|
||||||
async ({ body, set }) => {
|
async ({ body, set }) => {
|
||||||
// Check if already exists
|
const result = await ensureSteamGame(body.steamAppId)
|
||||||
const [existing] = await db
|
|
||||||
.select()
|
|
||||||
.from(games)
|
|
||||||
.where(eq(games.steamAppId, body.steamAppId))
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
if (existing) {
|
if (!result.game) {
|
||||||
return { game: existing, created: false }
|
set.status = 500
|
||||||
|
return { error: "Failed to create or retrieve game" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch details from Steam
|
// If game already existed, return 200 with created:false
|
||||||
let details: SteamAppDetails | null = null
|
if (!result.created) {
|
||||||
try {
|
return { game: result.game, created: false }
|
||||||
const url = new URL("https://store.steampowered.com/api/appdetails/")
|
|
||||||
url.searchParams.set("appids", String(body.steamAppId))
|
|
||||||
url.searchParams.set("cc", "US")
|
|
||||||
url.searchParams.set("l", "en")
|
|
||||||
|
|
||||||
const res = await fetch(url.toString(), {
|
|
||||||
headers: { Accept: "application/json" },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const data = (await res.json()) as Record<
|
|
||||||
string,
|
|
||||||
{ success: boolean; data: SteamAppDetails }
|
|
||||||
>
|
|
||||||
const entry = data[String(body.steamAppId)]
|
|
||||||
if (entry?.success) {
|
|
||||||
details = entry.data
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject non-games (DLCs, soundtracks, demos, etc.)
|
// If sync failed but stub exists, still return 201 with error info
|
||||||
if (details?.type && details.type !== "game") {
|
if (result.error) {
|
||||||
set.status = 400
|
set.status = 201
|
||||||
return { error: `Not a game (type: ${details.type})` }
|
return { game: result.game, created: true, syncError: result.error }
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to fetch Steam appdetails:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = details?.name || `Steam App ${body.steamAppId}`
|
|
||||||
const developer = details?.developers?.[0] || null
|
|
||||||
const publisher = details?.publishers?.[0] || null
|
|
||||||
const genres = details?.genres?.map((g) => g.description) || []
|
|
||||||
const headerImage = details?.header_image || null
|
|
||||||
const capsuleImage =
|
|
||||||
`https://cdn.akamai.steamstatic.com/steam/apps/${body.steamAppId}/library_600x900.jpg`
|
|
||||||
|
|
||||||
let finalCapsuleImage: string | null = capsuleImage
|
|
||||||
const imageValid = await validateImageUrl(capsuleImage)
|
|
||||||
|
|
||||||
if (!imageValid) {
|
|
||||||
const fallbackImage = await fetchSteamGridDBCover(title)
|
|
||||||
if (fallbackImage) {
|
|
||||||
finalCapsuleImage = fallbackImage
|
|
||||||
} else {
|
|
||||||
finalCapsuleImage = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [game] = await db
|
|
||||||
.insert(games)
|
|
||||||
.values({
|
|
||||||
steamAppId: body.steamAppId,
|
|
||||||
source: "steam",
|
|
||||||
title,
|
|
||||||
developer,
|
|
||||||
publisher,
|
|
||||||
genres,
|
|
||||||
headerImage,
|
|
||||||
capsuleImage: finalCapsuleImage,
|
|
||||||
storeUrl: `https://store.steampowered.com/app/${body.steamAppId}`,
|
|
||||||
systemRequirements: details?.pc_requirements
|
|
||||||
? { minimum: details.pc_requirements.minimum || null, recommended: details.pc_requirements.recommended || null }
|
|
||||||
: null,
|
|
||||||
metacriticScore: details?.metacritic?.score ?? null,
|
|
||||||
metacriticUrl: details?.metacritic?.url ?? null,
|
|
||||||
recommendationsTotal: details?.recommendations?.total ?? null,
|
|
||||||
priceCurrent: details?.price_overview?.final ?? null,
|
|
||||||
priceInitial: details?.price_overview?.initial ?? null,
|
|
||||||
priceCurrency: details?.price_overview?.currency ?? null,
|
|
||||||
isFree: details?.is_free ?? false,
|
|
||||||
releaseDate: details?.release_date?.date ?? null,
|
|
||||||
categories: details?.categories?.map((c) => c.description) ?? null,
|
|
||||||
platforms: details?.platforms ?? null,
|
|
||||||
lastSync: new Date(),
|
|
||||||
syncStatus: "synced",
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
|
|
||||||
set.status = 201
|
set.status = 201
|
||||||
return { game, created: true }
|
return { game: result.game, created: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
|
|||||||
Reference in New Issue
Block a user