feat: add Steam Reviews display API with caching
- Create lib/api/steam-reviews.ts with in-memory cache (1hr TTL) - Proxy endpoint for fetching Steam reviews by game ID - Supports offset/limit/language query params - Registers route in API index and app router
This commit is contained in:
@@ -27,6 +27,7 @@ import { gameStatsRoutes } from "@/lib/api/game-stats"
|
|||||||
import { gamesManualRoutes } from "@/lib/api/games-manual"
|
import { gamesManualRoutes } from "@/lib/api/games-manual"
|
||||||
import { compareRoutes } from "@/lib/api/compare"
|
import { compareRoutes } from "@/lib/api/compare"
|
||||||
import { playabilityRoutes } from "@/lib/api/playability"
|
import { playabilityRoutes } from "@/lib/api/playability"
|
||||||
|
import { steamReviewRoutes } from "@/lib/api/steam-reviews"
|
||||||
import { gamesListingRoutes } from "@/lib/api/games-listing"
|
import { gamesListingRoutes } from "@/lib/api/games-listing"
|
||||||
import { steamgridProxyRoutes } from "@/lib/api/steamgrid-proxy"
|
import { steamgridProxyRoutes } from "@/lib/api/steamgrid-proxy"
|
||||||
|
|
||||||
@@ -104,6 +105,8 @@ export const app = new Elysia({ prefix: "/api" })
|
|||||||
.use(compareRoutes)
|
.use(compareRoutes)
|
||||||
// Playability
|
// Playability
|
||||||
.use(playabilityRoutes)
|
.use(playabilityRoutes)
|
||||||
|
// Steam reviews
|
||||||
|
.use(steamReviewRoutes)
|
||||||
// Root
|
// Root
|
||||||
.get("/", () => ({
|
.get("/", () => ({
|
||||||
name: "DeckyVault API",
|
name: "DeckyVault API",
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ export { steamgridProxyRoutes } from "./steamgrid-proxy"
|
|||||||
export { gamesManualRoutes } from "./games-manual"
|
export { gamesManualRoutes } from "./games-manual"
|
||||||
export { compareRoutes } from "./compare"
|
export { compareRoutes } from "./compare"
|
||||||
export { playabilityRoutes } from "./playability"
|
export { playabilityRoutes } from "./playability"
|
||||||
|
export { steamReviewRoutes } from "./steam-reviews"
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { Elysia } from "elysia"
|
||||||
|
import { db } from "@/lib/db/index"
|
||||||
|
import { games } from "@/lib/db/schema"
|
||||||
|
import { eq } from "drizzle-orm"
|
||||||
|
|
||||||
|
// In-memory cache for reviews (key -> { data, expires })
|
||||||
|
const reviewCache = new Map<
|
||||||
|
string,
|
||||||
|
{ data: any; expires: number }
|
||||||
|
>()
|
||||||
|
|
||||||
|
const CACHE_TTL = 60 * 60 * 1000 // 1 hour
|
||||||
|
|
||||||
|
interface SteamReview {
|
||||||
|
recommendationid: string
|
||||||
|
author: {
|
||||||
|
steamid: string
|
||||||
|
num_games_owned: number
|
||||||
|
num_reviews: number
|
||||||
|
playtime_forever: number
|
||||||
|
playtime_last_two_weeks: number
|
||||||
|
playtime_at_review: number
|
||||||
|
last_played: number
|
||||||
|
}
|
||||||
|
language: string
|
||||||
|
review: string
|
||||||
|
timestamp_created: number
|
||||||
|
timestamp_updated: number
|
||||||
|
voted_up: boolean
|
||||||
|
votes_up: number
|
||||||
|
votes_funny: number
|
||||||
|
comment_count: number
|
||||||
|
steam_purchase: boolean
|
||||||
|
received_for_free: boolean
|
||||||
|
written_during_early_access: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SteamReviewResponse {
|
||||||
|
success: number
|
||||||
|
query_summary: {
|
||||||
|
num_reviews: number
|
||||||
|
review_score_desc: string
|
||||||
|
total_positive: number
|
||||||
|
total_negative: number
|
||||||
|
total_reviews: number
|
||||||
|
}
|
||||||
|
reviews: SteamReview[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const steamReviewRoutes = new Elysia({ prefix: "/steam-reviews" })
|
||||||
|
|
||||||
|
// Get embedded Steam reviews for a game
|
||||||
|
.get(
|
||||||
|
"/:gameId",
|
||||||
|
async ({ params, query, set }) => {
|
||||||
|
const gameId = params.gameId
|
||||||
|
const offset = Number(query.offset) || 0
|
||||||
|
const limit = Math.min(Number(query.limit) || 10, 20)
|
||||||
|
const language = query.language || "english"
|
||||||
|
|
||||||
|
// Get Steam App ID
|
||||||
|
const [game] = await db
|
||||||
|
.select({ steamAppId: games.steamAppId })
|
||||||
|
.from(games)
|
||||||
|
.where(eq(games.id, gameId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!game?.steamAppId) {
|
||||||
|
set.status = 404
|
||||||
|
return { error: "Game not found or has no Steam App ID" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = `${game.steamAppId}-${language}-${offset}-${limit}`
|
||||||
|
const cached = reviewCache.get(cacheKey)
|
||||||
|
if (cached && cached.expires > Date.now()) {
|
||||||
|
return cached.data
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://store.steampowered.com/appreviews/${game.steamAppId}?json=1&language=${language}&purchase_type=all&num_per_page=${limit}&cursor=${encodeURIComponent(Buffer.from(`${offset}`).toString("base64"))}&filter=recent&review_type=all`,
|
||||||
|
{ signal: AbortSignal.timeout(10000) }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
set.status = 502
|
||||||
|
return { error: "Failed to fetch Steam reviews" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: SteamReviewResponse = await response.json()
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
reviewCache.set(cacheKey, { data, expires: Date.now() + CACHE_TTL })
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
set.status = 502
|
||||||
|
return { error: "Steam review API unavailable" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user