Files
deckyvault/lib/api/steamdb-version.ts
T
adrianbonpin e5f671a992 feat: add multi-strategy version fetchers, disable auto-fetch by default
- Add 4 version-fetch strategies (UpToDateCheck API, Store Page Scrape,
  Community Hub Scrape, Store API Heuristic) — no Steam API key needed
- Create orchestrator that tries all strategies, prefers named version
  over build ID
- Add test page at /test-version-fetchers for comparing strategies
- Add standalone /api/version-test endpoint (direct Steam App ID, no DB)
- Update /api/games/:id/steamdb-version to use new strategies
- Disable auto-fetch on submit page by default (NEXT_PUBLIC_VERSION_AUTO_FETCH=true to enable)
- SteamDB scraping blocked by Cloudflare; strategies preserved for later enablement
2026-05-16 20:13:23 +08:00

173 lines
5.0 KiB
TypeScript

import { Elysia, t } from "elysia"
import { db } from "@/lib/db/index"
import { games } from "@/lib/db/schema"
import { eq } from "drizzle-orm"
import { fetchAllVersions, SERVER_STRATEGIES, CLIENT_STRATEGIES, type VersionFetchResult } from "@/lib/version-fetchers/index"
export const steamdbVersionRoutes = new Elysia({
prefix: "/games/:gameId",
detail: { tags: ["Games"] },
}).get(
"/steamdb-version",
async ({ params, set }) => {
// Look up game — supports both DB UUID and numeric Steam App ID
const isNumeric = /^\d+$/.test(params.gameId)
let steamAppId: number | null = null
if (isNumeric) {
// Already a Steam App ID — use directly
steamAppId = Number(params.gameId)
} else {
// Look up by DB UUID
const [game] = await db
.select({ steamAppId: games.steamAppId })
.from(games)
.where(eq(games.id, params.gameId))
.limit(1)
if (!game) {
set.status = 404
return { error: "Game not found" }
}
steamAppId = game.steamAppId
}
if (steamAppId === null) {
return { unavailable: true, reason: "no_steam_app_id" }
}
// Run ONLY server-safe strategies (no client-preferred ones)
// Client-side strategies should be called from the browser
const { best, all } = await fetchAllVersions(
steamAppId,
SERVER_STRATEGIES,
)
// If server strategies found nothing, tell the client which strategies to try
const clientStrategies = best.versionString || best.buildId
? [] // Server got something, client doesn't need to try more
: CLIENT_STRATEGIES.map((s) => s.name) // Server got nothing, suggest client try these
if (best.versionString === null && best.buildId === null && clientStrategies.length === 0) {
return {
unavailable: true,
reason: "not_found",
results: all.map((r) => ({
source: r.source,
success: r.success,
error: r.error,
})),
}
}
return {
versionString: best.versionString,
buildId: best.buildId,
steamAppId,
source: best.source,
clientStrategies,
// Include detailed results for debugging
results: all.map((r) => ({
source: r.source,
versionString: r.versionString,
buildId: r.buildId,
success: r.success,
error: r.error,
})),
// If server didn't find named version, tell client to try
needsClientFetch: clientStrategies.length > 0,
}
},
{
params: t.Object({ gameId: t.String() }),
},
)
/**
* Client-friendly version fetch endpoint.
* POST /api/games/:gameId/fetch-version-client
*
* Accepts results from client-side strategies and merges with server results.
* The client calls this after running client-side strategies in the browser.
*/
export const clientVersionRoutes = new Elysia({
prefix: "/games/:gameId",
detail: { tags: ["Games"] },
}).post(
"/fetch-version-client",
async ({ params, body, set }) => {
// Look up game — supports both DB UUID and numeric Steam App ID
const isNumeric = /^\d+$/.test(params.gameId)
let steamAppId: number | null = null
if (isNumeric) {
steamAppId = Number(params.gameId)
} else {
const [game] = await db
.select({ steamAppId: games.steamAppId })
.from(games)
.where(eq(games.id, params.gameId))
.limit(1)
if (!game) {
set.status = 404
return { error: "Game not found" }
}
steamAppId = game.steamAppId
}
if (steamAppId === null) {
return { unavailable: true, reason: "no_steam_app_id" }
}
// Run server strategies
const { best: serverBest } = await fetchAllVersions(
steamAppId,
SERVER_STRATEGIES,
)
// Merge with client results
const clientResults: VersionFetchResult[] = (body.clientResults ?? []).map(
(r: { source: string; versionString: string | null; buildId: string | null; success: boolean; error?: string }) => ({
...r,
}),
)
const allResults = [
...SERVER_STRATEGIES.map((s) => {
const existing = clientResults.find((r) => r.source === s.name)
return existing ?? { versionString: null, buildId: null, source: s.name, success: false }
}),
...clientResults.filter((r) => !SERVER_STRATEGIES.some((s) => s.name === r.source)),
]
// Find best: prefer versionString > buildId
let best: VersionFetchResult = serverBest
for (const r of allResults) {
if (r.versionString && !best.versionString) best = r
if (!best.versionString && !best.buildId && r.buildId) best = r
}
return {
versionString: best.versionString,
buildId: best.buildId,
source: best.source,
allResults,
}
},
{
params: t.Object({ gameId: t.String() }),
body: t.Object({
clientResults: t.Array(
t.Object({
source: t.String(),
versionString: t.Nullable(t.String()),
buildId: t.Nullable(t.String()),
success: t.Boolean(),
error: t.Optional(t.String()),
}),
),
}),
},
)