Add caching to PSN and Steam API endpoints

This commit is contained in:
Justin Edmund 2024-08-05 23:38:31 -07:00
parent 1bde04c2f7
commit e98532daaf
2 changed files with 88 additions and 36 deletions

View file

@ -1,34 +1,38 @@
import 'dotenv/config' import 'dotenv/config'
import type { AuthTokensResponse, RecentlyPlayedGamesResponse } from 'psn-api' import Module from 'node:module'
import redis from '../redis-client'
import type {
AuthTokensResponse,
GetUserPlayedTimeResponse,
RecentlyPlayedGamesResponse
} from 'psn-api'
import type { RequestHandler } from './$types' import type { RequestHandler } from './$types'
import Module from 'node:module'
const require = Module.createRequire(import.meta.url) const require = Module.createRequire(import.meta.url)
const { const {
exchangeNpssoForCode, exchangeNpssoForCode,
exchangeCodeForAccessToken, exchangeCodeForAccessToken,
getRecentlyPlayedGames getRecentlyPlayedGames,
getUserPlayedTime
} = require('psn-api') } = require('psn-api')
const CACHE_TTL = 60 * 60 * 24
const PSN_NPSSO_TOKEN = process.env.PSN_NPSSO_TOKEN const PSN_NPSSO_TOKEN = process.env.PSN_NPSSO_TOKEN
const PSN_ID = '1275018559140296533'
export const GET: RequestHandler = async ({ url }) => { export const GET: RequestHandler = async ({ url }) => {
let authorization = await authorize(PSN_NPSSO_TOKEN || '') // Check if data is in cache
const cachedData = await redis.get(`psn:${PSN_ID}`)
const response: RecentlyPlayedGamesResponse = await getRecentlyPlayedGames(authorization, { if (cachedData) {
limit: 5, console.log('Using cached PSN data')
categories: ['ps4_game', 'ps5_native_game'] return new Response(cachedData, {
}) headers: { 'Content-Type': 'application/json' }
const games: SerializableGameInfo[] = response.data.gameLibraryTitlesRetrieve.games.map(
(game) => ({
id: game.productId,
name: game.name,
playtime: undefined,
lastPlayed: new Date(game.lastPlayedDateTime),
coverURL: game.image.url
}) })
) }
// If not in cache, fetch and cache the data
const games = await getSerializedGames(PSN_ID)
return new Response(JSON.stringify(games), { return new Response(JSON.stringify(games), {
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
@ -40,3 +44,25 @@ async function authorize(npsso: string): Promise<AuthTokensResponse> {
const authorization = await exchangeCodeForAccessToken(accessCode) const authorization = await exchangeCodeForAccessToken(accessCode)
return authorization return authorization
} }
async function getSerializedGames(psnId: string): Promise<SerializableGameInfo[]> {
// Authorize with PSN and get games sorted by last played time
let authorization = await authorize(PSN_NPSSO_TOKEN || '')
const response = await getUserPlayedTime(authorization, PSN_ID, {
limit: 5,
categories: ['ps4_game', 'ps5_native_game']
})
// Map the games to a serializable format that the frontend understands.
const games: SerializableGameInfo[] = response.titles.map((game: GetUserPlayedTimeResponse) => ({
id: game.concept.id,
name: game.name,
playtime: game.playDuration,
lastPlayed: game.lastPlayedDateTime,
coverURL: game.imageUrl,
platform: 'psn'
}))
await redis.setex(`psn:${PSN_ID}`, CACHE_TTL, JSON.stringify(games))
return games
}

View file

@ -1,31 +1,30 @@
import { error, json } from '@sveltejs/kit' import { error, json } from '@sveltejs/kit'
import type { RequestHandler } from './$types' import redis from '../redis-client'
import SteamAPI, { Game, GameInfo, GameInfoExtended, UserPlaytime } from 'steamapi' import SteamAPI, { Game, GameInfo, GameInfoExtended, UserPlaytime } from 'steamapi'
import type { RequestHandler } from './$types'
const CACHE_TTL = 60 * 60 * 24 // 24 hours in seconds
const STEAM_ID = '76561197997279808'
export const GET: RequestHandler = async ({ params }) => { export const GET: RequestHandler = async ({ params }) => {
const steam = new SteamAPI(process.env.STEAM_API_KEY || '')
try { try {
const steamId = '76561197997279808' // Check if data is in cache
const cachedData = await redis.get(`steam:${STEAM_ID}`)
if (cachedData) {
console.log('Using cached Steam data')
return new Response(cachedData, {
headers: { 'Content-Type': 'application/json' }
})
}
const ownedGames = await steam.getUserOwnedGames(steamId, { includeExtendedAppInfo: true }) // If not in cache, fetch and cache the data
const games = await getSerializedGames(STEAM_ID)
const sortedGames = sortUserPlaytimes(ownedGames).slice(0, 5) return new Response(JSON.stringify(games), {
const extendedGames = sortedGames.filter(
(game): game is UserPlaytime<GameInfoExtended> => 'coverURL' in game.game
)
const serializableGames: SerializableGameInfo[] = extendedGames.map((game) => ({
id: game.game.id,
name: game.game.name,
playtime: game.minutes,
lastPlayed: game.lastPlayedAt,
coverURL: game.game.coverURL
}))
return new Response(JSON.stringify(serializableGames), {
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}) })
} catch (err) { } catch (err) {
console.log('Catching here')
console.error('Error fetching recent game:', err) console.error('Error fetching recent game:', err)
throw error(500, 'Error fetching recent game data') throw error(500, 'Error fetching recent game data')
} }
@ -51,3 +50,30 @@ function sortUserPlaytimes(
return b.minutes - a.minutes return b.minutes - a.minutes
}) })
} }
async function getSerializedGames(steamId: string): Promise<SerializableGameInfo[]> {
// Fetch all owned games from Steam
// This is necessary because the recently played API only returns games played in the last 14 days.
const steam = new SteamAPI(process.env.STEAM_API_KEY || '')
const steamGames = await steam.getUserOwnedGames(steamId, { includeExtendedAppInfo: true })
// Sort games based on when they were last played and take the first five games.
// Then, ensure that we use the getter method to fetch the cover URL.
const sortedGames = sortUserPlaytimes(steamGames).slice(0, 5)
const extendedGames = sortedGames.filter(
(game): game is UserPlaytime<GameInfoExtended> => 'coverURL' in game.game
)
// Map the games to a serializable format that the frontend understands.
let games: SerializableGameInfo[] = extendedGames.map((game) => ({
id: game.game.id,
name: game.game.name,
playtime: game.minutes,
lastPlayed: game.lastPlayedAt,
coverURL: game.game.coverURL,
platform: 'steam'
}))
await redis.setex(`steam:${STEAM_ID}`, CACHE_TTL, JSON.stringify(games))
return games
}