From 6d46ef48d00baf9faf64cca3748ac91021a592f6 Mon Sep 17 00:00:00 2001 From: creations Date: Sun, 20 Apr 2025 14:39:15 -0400 Subject: [PATCH] add redis, game icons and fix readme --- .env.example | 6 +++ README.md | 15 +++++-- config/environment.ts | 6 +++ public/css/index.css | 4 ++ public/js/index.js | 39 +++++++++++++++--- src/routes/api/art[game].ts | 82 +++++++++++++++++++++++++++++++++++++ src/views/index.ejs | 22 ++++------ 7 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 src/routes/api/art[game].ts diff --git a/.env.example b/.env.example index bf1f90d..689378b 100644 --- a/.env.example +++ b/.env.example @@ -2,9 +2,15 @@ HOST=0.0.0.0 PORT=8080 +REDIS_URL=redis://username:password@localhost:6379 +REDIS_TTL=3600 # seconds + # this is only the default value if non is give in /id LANYARD_USER_ID=id-here LANYARD_INSTANCE=https://lanyard.rest # Required if you want to enable badges BADGE_API_URL=http://localhost:8081 + +# https://www.steamgriddb.com/api/v2, if you want games to have images +STEAMGRIDDB_API_KEY=steamgrid_api_key diff --git a/README.md b/README.md index 5e6644c..aa09058 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,20 @@ A cool little web app that shows your Discord profile, current activity, and more. Built with Bun and EJS. -## Prerequisite: Lanyard Backend +## Requirements + +This project relies on the following services to function correctly: + +### 1. Lanyard Backend This project depends on a self-hosted or public [Lanyard](https://github.com/Phineas/lanyard) instance for Discord presence data. - Make sure Lanyard is running and accessible before using this profile page. +### 2. Redis Instance + +A Redis-compatible key-value store is required for caching third-party data (e.g., SteamGridDB icons). +We recommend using [Dragonfly](https://www.dragonflydb.io/) as a high-performance drop-in replacement for Redis. + --- ## Getting Started @@ -34,9 +42,10 @@ cp .env.example .env |--------------------|--------------------------------------------------| | `HOST` | Host to bind the Bun server (default: `0.0.0.0`) | | `PORT` | Port to run the server on (default: `8080`) | +| `REDIS_URL` | Redis connection string | | `LANYARD_USER_ID` | Your Discord user ID | | `LANYARD_INSTANCE` | Lanyard WebSocket endpoint URL | -| `BADGE_API_URL` | Uses the [badge api](https://git.creations.works/creations/badgeAPI) only required if you want to use badges +| `BADGE_API_URL` | Uses the [badge api](https://git.creations.works/creations/badgeAPI) only required if you want to use badges | #### Optional Lanyard KV Vars (per-user customization) diff --git a/config/environment.ts b/config/environment.ts index 1476062..a7344f9 100644 --- a/config/environment.ts +++ b/config/environment.ts @@ -5,9 +5,15 @@ export const environment: Environment = { process.env.NODE_ENV === "development" || process.argv.includes("--dev"), }; +export const redisTtl: number = process.env.REDIS_TTL + ? Number.parseInt(process.env.REDIS_TTL, 10) + : 60 * 60 * 1; // 1 hour + export const lanyardConfig: LanyardConfig = { userId: process.env.LANYARD_USER_ID || "", instance: process.env.LANYARD_INSTANCE || "https://api.lanyard.rest", }; export const badgeApi: string | null = process.env.BADGE_API_URL || null; +export const steamGridDbKey: string | undefined = + process.env.STEAMGRIDDB_API_KEY; diff --git a/public/css/index.css b/public/css/index.css index d7e31e6..4728251 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -316,6 +316,10 @@ ul { height: 80px; } +.no-asset { + display: none !important; +} + .activity-image-small { width: 25px; height: 25px; diff --git a/public/js/index.js b/public/js/index.js index 74dd8cb..998bb3d 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -240,12 +240,15 @@ function buildActivityHTML(activity) { const secondaryLine = isMusic ? activity.state : activity.details; const tertiaryLine = isMusic ? activity.assets?.large_text : activity.state; - const activityArt = art - ? `
- Art - ${smallArt ? `Small Art` : ""} + const activityArt = `
+ + ${``}
` - : ""; return `
  • @@ -423,5 +426,31 @@ function updatePresence(data) { activitiesTitle.classList.add("hidden"); } updateElapsedAndProgress(); + getAllNoAsset(); + } +} + +async function getAllNoAsset() { + const noAssetImages = document.querySelectorAll("img.activity-image.no-asset"); + + console.log("Images with .no-asset:", noAssetImages.length, noAssetImages); + + for (const img of noAssetImages) { + const name = img.dataset.name; + if (!name) continue; + + try { + const res = await fetch(`/api/art/${encodeURIComponent(name)}`); + if (!res.ok) continue; + + const { icon } = await res.json(); + if (icon) { + img.src = icon; + img.classList.remove("no-asset"); + img.parentElement.classList.remove("no-asset"); + } + } catch (err) { + console.warn(`Failed to fetch fallback icon for "${name}"`, err); + } } } diff --git a/src/routes/api/art[game].ts b/src/routes/api/art[game].ts new file mode 100644 index 0000000..3a945f6 --- /dev/null +++ b/src/routes/api/art[game].ts @@ -0,0 +1,82 @@ +import { redisTtl, steamGridDbKey } from "@config/environment"; +import { logger } from "@helpers/logger"; +import { redis } from "bun"; + +if (!steamGridDbKey) { + logger.warn("[SteamGridDB] Route disabled: Missing API key"); +} + +const routeDef: RouteDef = { + method: "GET", + accepts: "*/*", + returns: "application/json", +}; + +async function fetchSteamGridIcon(gameName: string): Promise { + const cacheKey = `steamgrid:icon:${gameName.toLowerCase()}`; + const cached = await redis.get(cacheKey); + if (cached) return cached; + + const search = await fetch(`https://www.steamgriddb.com/api/v2/search/autocomplete/${encodeURIComponent(gameName)}`, { + headers: { + Authorization: `Bearer ${steamGridDbKey}`, + }, + }); + + if (!search.ok) return null; + + const { data } = await search.json(); + if (!data?.length) return null; + + const gameId = data[0]?.id; + if (!gameId) return null; + + const iconRes = await fetch(`https://www.steamgriddb.com/api/v2/icons/game/${gameId}`, { + headers: { + Authorization: `Bearer ${steamGridDbKey}`, + }, + }); + + if (!iconRes.ok) return null; + + const iconData = await iconRes.json(); + const icon = iconData?.data?.[0]?.url ?? null; + + if (icon) { + await redis.set(cacheKey, icon); + await redis.expire(cacheKey, redisTtl); + } + return icon; +} + +async function handler(request: ExtendedRequest): Promise { + if (!steamGridDbKey) { + return Response.json( + { status: 503, error: "Route disabled due to missing SteamGridDB key" }, + { status: 503 } + ); + } + + const { game } = request.params; + + if (!game || typeof game !== "string" || game.length < 2) { + return Response.json({ status: 400, error: "Missing or invalid game name" }, { status: 400 }); + } + + const icon = await fetchSteamGridIcon(game); + + if (!icon) { + return Response.json({ status: 404, error: "Icon not found" }, { status: 404 }); + } + + return Response.json( + { + status: 200, + game, + icon, + }, + { status: 200 } + ); +} + +export { handler, routeDef }; diff --git a/src/views/index.ejs b/src/views/index.ejs index 8194055..d704836 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -32,9 +32,9 @@ -<%- include("partial/style.ejs") %> - + <%- include("partial/style.ejs") %> +
    @@ -54,10 +54,10 @@ <% if (activities.length && activities[0].type === 4) { @@ -158,14 +158,10 @@ <% } %>
    - <% if (art) { %> -
    - Art> - <% if (smallArt) { %> - Small Art> - <% } %> +
    "> + " <%= activity.assets?.large_text ? `title="${activity.assets.large_text}"` : '' %>> + " src="<%= smallArt %>" <%= activity.assets?.small_text ? `title="${activity.assets.small_text}"` : '' %>>
    - <% } %>
    <%