songguess / server /server.js
Excalibro's picture
Upload folder using huggingface_hub
1951aa6 verified
import dotenv from "dotenv";
import cors from "cors";
import express from "express";
import crypto from "node:crypto";
import fs from "node:fs";
import fsp from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
dotenv.config({
path: new URL("../.env", import.meta.url)
});
const app = express();
const execFileAsync = promisify(execFile);
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const clientDistDir = path.join(rootDir, "client", "dist");
const clipCacheDir = path.join(os.tmpdir(), "song-sprint-clips");
const genreLibraryPath = "/data/data/com.termux/files/home/storage/downloads/guessgenres.json";
const port = Number(process.env.PORT || 3001);
const clientId = process.env.VITE_CLIENT_ID;
const clientSecret = process.env.DISCORD_CLIENT_SECRET;
const redirectUri = process.env.DISCORD_REDIRECT_URI || "https://127.0.0.1";
const allowedOrigins = new Set(
(process.env.ALLOWED_ORIGINS || "")
.split(",")
.map((value) => value.trim())
.filter(Boolean)
);
const sessions = new Map();
const rooms = new Map();
const clipJobs = new Map();
const REVEAL_SECONDS = 4;
let lastAuthDebug = null;
let genreLibrary = {};
app.use(cors({
origin(origin, callback) {
if (!origin || allowedOrigins.size === 0 || allowedOrigins.has(origin)) {
callback(null, true);
return;
}
callback(new Error(`Origin not allowed: ${origin}`));
}
}));
app.use(express.json());
app.get("/api/config", (_req, res) => {
res.json({
clientId: clientId || "",
apiBaseUrl: process.env.VITE_API_BASE_URL || ""
});
});
app.get("/api/genres", (_req, res) => {
res.json({
genres: listGenres()
});
});
function randomId() {
return crypto.randomBytes(16).toString("hex");
}
function slugTitle(value) {
return value.toLowerCase().replaceAll(/[^a-z0-9]+/g, " ").trim();
}
function parsePlaylistId(input) {
if (!input) {
return null;
}
const trimmed = input.trim();
const directMatch = trimmed.match(/^[A-Za-z0-9_-]{10,}$/);
if (directMatch) {
return directMatch[0];
}
const embeddedMatch = trimmed.match(/[?&]list=([A-Za-z0-9_-]{10,})/i)
|| trimmed.match(/(?:^|\\s|\\b)list=([A-Za-z0-9_-]{10,})/i)
|| trimmed.match(/youtube\.com\/playlist\?list=([A-Za-z0-9_-]{10,})/i)
|| trimmed.match(/music\.youtube\.com\/playlist\?list=([A-Za-z0-9_-]{10,})/i);
if (embeddedMatch) {
return embeddedMatch[1];
}
try {
const url = new URL(trimmed);
return url.searchParams.get("list");
} catch {
return null;
}
}
function parseHumanDuration(value) {
if (!value) {
return null;
}
const parts = value
.split(":")
.map((part) => Number(part.trim()))
.filter((part) => Number.isFinite(part));
if (!parts.length) {
return null;
}
let seconds = 0;
for (const part of parts) {
seconds = seconds * 60 + part;
}
return seconds;
}
function shuffle(items) {
const clone = [...items];
for (let i = clone.length - 1; i > 0; i -= 1) {
const nextIndex = Math.floor(Math.random() * (i + 1));
[clone[i], clone[nextIndex]] = [clone[nextIndex], clone[i]];
}
return clone;
}
function uniqueBy(items, keyFn) {
const seen = new Set();
return items.filter((item) => {
const key = keyFn(item);
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
async function fetchJson(url, init) {
const response = await fetch(url, init);
const data = await response.json();
if (!response.ok) {
const error = new Error(data.error?.message || data.error || "Request failed");
error.status = response.status;
error.payload = data;
throw error;
}
return data;
}
async function fetchText(url, init) {
const response = await fetch(url, init);
const text = await response.text();
if (!response.ok) {
const error = new Error(`Request failed with ${response.status}.`);
error.status = response.status;
error.payload = text;
throw error;
}
return text;
}
async function fetchWithRetry(url, init, attempts = 3) {
let lastError = null;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await fetch(url, init);
} catch (error) {
lastError = error;
if (attempt < attempts) {
await new Promise((resolve) => setTimeout(resolve, attempt * 500));
}
}
}
throw lastError;
}
function loadGenreLibrary() {
try {
const raw = fs.readFileSync(genreLibraryPath, "utf8");
const parsed = JSON.parse(raw);
genreLibrary = Object.fromEntries(
Object.entries(parsed).filter(([, playlists]) => Array.isArray(playlists) && playlists.length > 0)
);
} catch {
genreLibrary = {};
}
}
function listGenres() {
return Object.entries(genreLibrary).map(([key, playlists]) => ({
key,
label: key.replaceAll(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()),
playlistCount: playlists.length
}));
}
function getPlaylistsForGenre(genreKey) {
if (genreKey === "all") {
return Object.values(genreLibrary).flat();
}
return genreLibrary[genreKey] || [];
}
async function ensureClipCacheDir() {
await fsp.mkdir(clipCacheDir, { recursive: true });
}
function getRoundAudioKey(round) {
return `${round.videoId}_${round.startSeconds}_${round.clipDurationSeconds}`.replaceAll(/[^A-Za-z0-9._-]+/g, "_");
}
async function createRoundAudioClip(round) {
await ensureClipCacheDir();
const clipKey = getRoundAudioKey(round);
const outputPath = path.join(clipCacheDir, `${clipKey}.mp3`);
try {
await fsp.access(outputPath);
return outputPath;
} catch {
// continue
}
if (clipJobs.has(clipKey)) {
return clipJobs.get(clipKey);
}
const job = (async () => {
const videoUrl = `https://www.youtube.com/watch?v=${round.videoId}`;
let stdout;
try {
({ stdout } = await execFileAsync("yt-dlp", [
"-f", "bestaudio",
"--no-playlist",
"--extractor-args", "youtube:player_client=android,web,tv_embedded",
"-g",
videoUrl
]));
} catch (error) {
const stderr = typeof error.stderr === "string" ? error.stderr.trim() : "";
const message = stderr || error.message || "yt-dlp failed to resolve the YouTube audio stream.";
throw new Error(message);
}
const mediaUrl = stdout.trim().split("\n").find(Boolean);
if (!mediaUrl) {
throw new Error("Could not resolve YouTube audio stream.");
}
await execFileAsync("ffmpeg", [
"-y",
"-ss", String(round.startSeconds),
"-t", String(round.clipDurationSeconds),
"-i", mediaUrl,
"-vn",
"-acodec", "libmp3lame",
"-ar", "44100",
"-ac", "2",
outputPath
]);
return outputPath;
})();
clipJobs.set(clipKey, job);
try {
return await job;
} finally {
clipJobs.delete(clipKey);
}
}
function getTextFromRuns(value) {
if (!value) {
return null;
}
if (typeof value.simpleText === "string") {
return value.simpleText;
}
if (Array.isArray(value.runs)) {
return value.runs.map((entry) => entry.text || "").join("").trim();
}
return null;
}
function collectPlaylistVideoRenderers(node, bucket) {
if (!node || typeof node !== "object") {
return;
}
if (Array.isArray(node)) {
for (const entry of node) {
collectPlaylistVideoRenderers(entry, bucket);
}
return;
}
if (node.playlistVideoRenderer) {
bucket.push(node.playlistVideoRenderer);
}
for (const value of Object.values(node)) {
collectPlaylistVideoRenderers(value, bucket);
}
}
function extractInitialData(html) {
const patterns = [
/var ytInitialData = (\{.*?\});<\/script>/s,
/window\["ytInitialData"\] = (\{.*?\});<\/script>/s,
/ytInitialData"\] = (\{.*?\});<\/script>/s
];
for (const pattern of patterns) {
const match = html.match(pattern);
if (match) {
return JSON.parse(match[1]);
}
}
throw new Error("Could not extract playlist data from YouTube page.");
}
async function fetchPlaylistTracks(playlistId) {
const html = await fetchText(`https://www.youtube.com/playlist?list=${encodeURIComponent(playlistId)}`, {
headers: {
"User-Agent": "Mozilla/5.0"
}
});
const initialData = extractInitialData(html);
const renderers = [];
collectPlaylistVideoRenderers(initialData, renderers);
const entries = renderers.map((renderer) => ({
videoId: renderer.videoId || null,
title: getTextFromRuns(renderer.title),
thumbnailUrl: renderer.thumbnail?.thumbnails?.at(-1)?.url || null,
durationSeconds: Number(renderer.lengthSeconds) || parseHumanDuration(getTextFromRuns(renderer.lengthText)),
isPlayable: renderer.isPlayable !== false
}));
return uniqueBy(
entries.filter((entry) => (
entry.videoId &&
entry.title &&
entry.title !== "Deleted video" &&
entry.title !== "Private video" &&
entry.isPlayable
)),
(entry) => entry.videoId
);
}
function ensureRoom(roomId) {
if (!rooms.has(roomId)) {
rooms.set(roomId, {
id: roomId,
hostUserId: null,
settings: {
genreKey: "all",
roundsCount: 8,
clipDurationSeconds: 12
},
playlist: [],
playlistSummary: null,
players: new Map(),
game: {
status: "lobby",
currentRoundIndex: 0,
rounds: []
}
});
}
return rooms.get(roomId);
}
function requireSession(req) {
const token = req.header("X-Session-Token");
const session = token ? sessions.get(token) : null;
if (!session) {
const error = new Error("Missing or invalid session.");
error.status = 401;
throw error;
}
return session;
}
function getSessionFromRequest(req) {
const headerToken = req.header("X-Session-Token");
const queryToken = typeof req.query.sessionToken === "string" ? req.query.sessionToken : null;
const token = headerToken || queryToken;
const session = token ? sessions.get(token) : null;
if (!session) {
const error = new Error("Missing or invalid session.");
error.status = 401;
throw error;
}
return session;
}
function ensurePlayer(room, session) {
if (!room.players.has(session.user.id)) {
room.players.set(session.user.id, {
userId: session.user.id,
displayName: session.user.global_name || session.user.username,
score: 0,
lastSeenAt: Date.now()
});
} else {
const player = room.players.get(session.user.id);
player.displayName = session.user.global_name || session.user.username;
player.lastSeenAt = Date.now();
}
if (!room.hostUserId) {
room.hostUserId = session.user.id;
}
}
function selectFakeOptions(pool, correctTitle) {
const correctSlug = slugTitle(correctTitle);
const fakeTitles = uniqueBy(
pool.filter((entry) => slugTitle(entry.title) !== correctSlug),
(entry) => slugTitle(entry.title)
).map((entry) => entry.title);
return shuffle(fakeTitles).slice(0, 3);
}
function pointsForCorrectPosition(position) {
if (position === 0) {
return 120;
}
if (position === 1) {
return 85;
}
return 60;
}
function buildRounds(room) {
const { roundsCount, clipDurationSeconds } = room.settings;
const eligibleTracks = room.playlist.filter((track) => (
track.durationSeconds === null ||
track.durationSeconds === undefined ||
track.durationSeconds >= clipDurationSeconds + 2
));
if (eligibleTracks.length < roundsCount || room.playlist.length < 4) {
const error = new Error("Playlist needs enough playable videos to cover the requested rounds.");
error.status = 400;
throw error;
}
const selectedTracks = shuffle(eligibleTracks).slice(0, roundsCount);
return selectedTracks.map((track, index) => {
const fakeTitles = selectFakeOptions(room.playlist, track.title);
if (fakeTitles.length < 3) {
const error = new Error("Playlist needs at least four distinct song titles.");
error.status = 400;
throw error;
}
const options = shuffle([
{ id: `real:${track.videoId}`, label: track.title },
...fakeTitles.map((title, fakeIndex) => ({ id: `fake:${index}:${fakeIndex}`, label: title }))
]);
const correctOption = options.find((option) => option.id === `real:${track.videoId}`);
const knownDuration = Number.isFinite(track.durationSeconds) ? track.durationSeconds : null;
const maxStart = knownDuration === null
? Math.max(0, clipDurationSeconds * 2)
: Math.max(0, knownDuration - clipDurationSeconds - 1);
return {
roundNumber: index + 1,
videoId: track.videoId,
title: track.title,
thumbnailUrl: track.thumbnailUrl,
clipDurationSeconds: knownDuration !== null
? Math.min(clipDurationSeconds, Math.max(5, knownDuration))
: clipDurationSeconds,
startSeconds: maxStart > 0 ? Math.floor(Math.random() * (maxStart + 1)) : 0,
options,
correctOptionId: correctOption.id,
guesses: [],
startedAt: null,
revealAt: null,
nextRoundAt: null
};
});
}
function startRound(room, roundIndex, startedAt) {
const round = room.game.rounds[roundIndex];
round.startedAt = startedAt;
round.revealAt = startedAt + round.clipDurationSeconds * 1000;
round.nextRoundAt = round.revealAt + REVEAL_SECONDS * 1000;
}
function syncRoom(room) {
if (room.game.status !== "playing") {
return;
}
const now = Date.now();
while (room.game.status === "playing") {
const round = room.game.rounds[room.game.currentRoundIndex];
if (!round) {
room.game.status = "finished";
break;
}
if (round.startedAt === null) {
break;
}
if (now < round.nextRoundAt) {
break;
}
room.game.currentRoundIndex += 1;
if (room.game.currentRoundIndex >= room.game.rounds.length) {
room.game.status = "finished";
room.game.currentRoundIndex = room.game.rounds.length - 1;
break;
}
break;
}
}
function serializeRound(round, userId) {
if (!round) {
return null;
}
const now = Date.now();
const phase = round.startedAt === null
? "pending"
: now < round.revealAt
? "guessing"
: "reveal";
const myGuess = round.guesses.find((guess) => guess.userId === userId);
const correctGuesses = round.guesses.filter((guess) => guess.correct);
const fastestCorrect = correctGuesses[0] || null;
return {
roundNumber: round.roundNumber,
phase,
secondsRemaining: phase === "pending"
? round.clipDurationSeconds
: Math.max(0, Math.ceil(((phase === "guessing" ? round.revealAt : round.nextRoundAt) - now) / 1000)),
clipDurationSeconds: round.clipDurationSeconds,
startSeconds: round.startSeconds,
videoId: round.videoId,
options: round.options,
hasGuessed: Boolean(myGuess),
myGuessOptionId: myGuess?.optionId || null,
correctOptionId: phase === "reveal" ? round.correctOptionId : null,
correctTitle: phase === "reveal" ? round.title : null,
fastestCorrectPlayerName: fastestCorrect?.displayName || null,
fastestCorrectPoints: fastestCorrect?.awardedPoints || 0
};
}
function serializeRoom(room, userId) {
syncRoom(room);
const players = [...room.players.values()].sort((left, right) => {
if (right.score !== left.score) {
return right.score - left.score;
}
return left.displayName.localeCompare(right.displayName);
});
return {
isHost: room.hostUserId === userId,
room: {
id: room.id,
name: room.id,
settings: room.settings,
playlistSummary: room.playlistSummary
},
players,
game: {
status: room.game.status,
currentRound: serializeRound(room.game.rounds[room.game.currentRoundIndex], userId)
}
};
}
app.get("/api/health", (_req, res) => {
res.json({ ok: true });
});
app.get("/api/config", (_req, res) => {
res.json({
clientId: clientId || null,
redirectUri,
allowedOrigins: [...allowedOrigins]
});
});
app.get("/api/debug/auth-error", (_req, res) => {
res.json(lastAuthDebug || { ok: true });
});
app.post("/api/token", async (req, res) => {
try {
lastAuthDebug = {
stage: "entered_token_route",
timestamp: new Date().toISOString()
};
if (!clientId || !clientSecret) {
res.status(500).json({ error: "Missing Discord OAuth environment variables." });
return;
}
const { code } = req.body ?? {};
if (!code) {
res.status(400).json({ error: "Missing OAuth authorization code." });
return;
}
const body = new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
grant_type: "authorization_code",
code,
redirect_uri: redirectUri
});
const tokenResponse = await fetchWithRetry("https://discord.com/api/v10/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body
});
const tokenText = await tokenResponse.text();
let tokenData;
try {
tokenData = JSON.parse(tokenText);
} catch {
tokenData = { error: "discord_token_exchange_non_json", raw: tokenText };
}
if (!tokenResponse.ok) {
lastAuthDebug = {
stage: "token_exchange",
status: tokenResponse.status,
redirectUri,
response: tokenData
};
res.status(tokenResponse.status).json(tokenData);
return;
}
const meResponse = await fetchWithRetry("https://discord.com/api/v10/users/@me", {
headers: {
Authorization: `Bearer ${tokenData.access_token}`
}
});
const meText = await meResponse.text();
let meData;
try {
meData = JSON.parse(meText);
} catch {
meData = { error: "discord_me_non_json", raw: meText };
}
if (!meResponse.ok) {
lastAuthDebug = {
stage: "user_lookup",
status: meResponse.status,
response: meData
};
res.status(meResponse.status).json(meData);
return;
}
const sessionToken = randomId();
sessions.set(sessionToken, {
accessToken: tokenData.access_token,
user: meData
});
lastAuthDebug = null;
res.json({
access_token: tokenData.access_token,
session_token: sessionToken,
user: meData
});
} catch (error) {
lastAuthDebug = {
stage: "exception",
message: error.message || "Discord token exchange failed.",
cause: error.cause?.message || null
};
res.status(error.status || 500).json({
error: error.message || "Discord token exchange failed.",
cause: error.cause?.message || undefined
});
}
});
app.post("/api/room/join", (req, res) => {
try {
const session = requireSession(req);
const roomId = req.body?.roomId;
if (!roomId) {
res.status(400).json({ error: "Missing roomId." });
return;
}
const room = ensureRoom(roomId);
ensurePlayer(room, session);
res.json(serializeRoom(room, session.user.id));
} catch (error) {
res.status(error.status || 500).json({ error: error.message });
}
});
app.get("/api/room/state", (req, res) => {
try {
const session = requireSession(req);
const room = ensureRoom(req.query.roomId);
ensurePlayer(room, session);
res.json(serializeRoom(room, session.user.id));
} catch (error) {
res.status(error.status || 500).json({ error: error.message });
}
});
app.post("/api/room/settings", async (req, res) => {
try {
const session = requireSession(req);
const room = ensureRoom(req.body?.roomId);
ensurePlayer(room, session);
if (room.hostUserId !== session.user.id) {
res.status(403).json({ error: "Only the host can update settings." });
return;
}
const genreKey = req.body?.genreKey?.trim() || "all";
const roundsCount = Number(req.body?.roundsCount);
const clipDurationSeconds = Number(req.body?.clipDurationSeconds);
if (genreKey !== "all" && !genreLibrary[genreKey]) {
res.status(400).json({ error: "Select a valid genre." });
return;
}
if (!Number.isInteger(roundsCount) || roundsCount < 3 || roundsCount > 20) {
res.status(400).json({ error: "Rounds must be between 3 and 20." });
return;
}
if (!Number.isInteger(clipDurationSeconds) || clipDurationSeconds < 5 || clipDurationSeconds > 30) {
res.status(400).json({ error: "Clip duration must be between 5 and 30 seconds." });
return;
}
const playlistUrls = getPlaylistsForGenre(genreKey);
const playlistIds = playlistUrls
.map((value) => parsePlaylistId(value))
.filter(Boolean);
if (!playlistIds.length) {
res.status(400).json({ error: "No playlists configured for that genre." });
return;
}
const playlistGroups = await Promise.all(playlistIds.map((playlistId) => fetchPlaylistTracks(playlistId)));
const playlist = uniqueBy(playlistGroups.flat(), (entry) => entry.videoId);
room.settings = {
genreKey,
roundsCount,
clipDurationSeconds
};
room.playlist = playlist;
room.playlistSummary = `${genreKey === "all" ? "All genres" : genreKey}${playlist.length} tracks • ${playlistIds.length} playlists`;
room.game = {
status: "lobby",
currentRoundIndex: 0,
rounds: []
};
res.json(serializeRoom(room, session.user.id));
} catch (error) {
res.status(error.status || 500).json({
error: error.payload?.error?.message || error.message
});
}
});
app.post("/api/room/start", (req, res) => {
try {
const session = requireSession(req);
const room = ensureRoom(req.body?.roomId);
ensurePlayer(room, session);
if (room.hostUserId !== session.user.id) {
res.status(403).json({ error: "Only the host can start the game." });
return;
}
room.game = {
status: "playing",
currentRoundIndex: 0,
rounds: buildRounds(room)
};
for (const player of room.players.values()) {
player.score = 0;
}
res.json(serializeRoom(room, session.user.id));
} catch (error) {
res.status(error.status || 500).json({ error: error.message });
}
});
app.post("/api/room/round-start", (req, res) => {
try {
const session = requireSession(req);
const room = ensureRoom(req.body?.roomId);
ensurePlayer(room, session);
syncRoom(room);
if (room.hostUserId !== session.user.id) {
res.status(403).json({ error: "Only the host can start the round." });
return;
}
if (room.game.status !== "playing") {
res.status(400).json({ error: "No active game." });
return;
}
const round = room.game.rounds[room.game.currentRoundIndex];
if (!round) {
res.status(400).json({ error: "No round available." });
return;
}
if (round.startedAt !== null) {
res.json(serializeRoom(room, session.user.id));
return;
}
startRound(room, room.game.currentRoundIndex, Date.now());
res.json(serializeRoom(room, session.user.id));
} catch (error) {
res.status(error.status || 500).json({ error: error.message });
}
});
app.post("/api/room/reset", (req, res) => {
try {
const session = requireSession(req);
const room = ensureRoom(req.body?.roomId);
ensurePlayer(room, session);
if (room.hostUserId !== session.user.id) {
res.status(403).json({ error: "Only the host can reset the room." });
return;
}
room.game = {
status: "lobby",
currentRoundIndex: 0,
rounds: []
};
for (const player of room.players.values()) {
player.score = 0;
}
res.json(serializeRoom(room, session.user.id));
} catch (error) {
res.status(error.status || 500).json({ error: error.message });
}
});
app.post("/api/room/guess", (req, res) => {
try {
const session = requireSession(req);
const room = ensureRoom(req.body?.roomId);
ensurePlayer(room, session);
syncRoom(room);
if (room.game.status !== "playing") {
res.status(400).json({ error: "No active round." });
return;
}
const round = room.game.rounds[room.game.currentRoundIndex];
if (!round || round.roundNumber !== Number(req.body?.roundNumber)) {
res.status(400).json({ error: "Round changed before the guess was received." });
return;
}
if (Date.now() >= round.revealAt) {
res.status(400).json({ error: "Round is already closed." });
return;
}
if (round.guesses.some((guess) => guess.userId === session.user.id)) {
res.status(400).json({ error: "You already answered this round." });
return;
}
const correct = round.correctOptionId === req.body?.optionId;
const player = room.players.get(session.user.id);
let awardedPoints = 0;
if (correct) {
const position = round.guesses.filter((guess) => guess.correct).length;
awardedPoints = pointsForCorrectPosition(position);
player.score += awardedPoints;
}
round.guesses.push({
userId: session.user.id,
displayName: player.displayName,
optionId: req.body?.optionId,
correct,
awardedPoints,
guessedAt: Date.now()
});
res.json({
ok: true,
correct,
awardedPoints
});
} catch (error) {
res.status(error.status || 500).json({ error: error.message });
}
});
app.get("/api/room/audio", async (req, res) => {
try {
const session = getSessionFromRequest(req);
const room = ensureRoom(req.query.roomId);
ensurePlayer(room, session);
syncRoom(room);
if (room.game.status !== "playing") {
res.status(400).json({ error: "No active game." });
return;
}
const roundNumber = Number(req.query.roundNumber);
const round = room.game.rounds.find((entry) => entry.roundNumber === roundNumber);
if (!round) {
res.status(404).json({ error: "Round not found." });
return;
}
const clipPath = await createRoundAudioClip(round);
res.setHeader("Content-Type", "audio/mpeg");
res.setHeader("Cache-Control", "public, max-age=86400");
fs.createReadStream(clipPath).pipe(res);
} catch (error) {
res.status(error.status || 500).json({ error: error.message });
}
});
if (process.env.NODE_ENV === "production") {
app.use((req, res, next) => {
if (req.path === "/" || req.path.endsWith(".html")) {
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
} else if (req.path.startsWith("/assets/")) {
res.setHeader("Cache-Control", "no-store, max-age=0");
}
next();
});
app.use(express.static(clientDistDir));
app.get("*", (req, res, next) => {
if (req.path.startsWith("/api/")) {
next();
return;
}
res.sendFile(path.join(clientDistDir, "index.html"));
});
}
app.use((error, req, res, _next) => {
lastAuthDebug = {
stage: "express_error",
path: req.path,
method: req.method,
message: error.message || "Unhandled server error"
};
res.status(error.status || 500).json({
error: error.message || "Unhandled server error"
});
});
loadGenreLibrary();
app.listen(port, () => {
console.log(`Discord Activity server listening on http://127.0.0.1:${port}`);
});