derek-thomas's picture
Removing extra categories
c2ad56d
Raw History Blame Contribute Delete
23.9 kB
import express from "express";
import "dotenv/config";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { checkRepoAccess, downloadFile, uploadFile } from "@huggingface/hub";
const DUPR_SEARCH_URL = "https://api.dupr.gg/player/v1.0/search";
const DUPR_PLAYER_URL = "https://api.dupr.gg/player/v1.0";
const PORT = Number(process.env.PORT || 7860);
const DEFAULT_LAT = 24.453884;
const DEFAULT_LNG = 54.3773438;
const EXPLORER_URL_PATTERN =
process.env.DUPR_EXPLORER_URL_PATTERN || "https://duprexplorer.com/player/{numericPlayerId}";
const HF_DATASET_REPO = process.env.HF_DATASET_REPO || "derek-thomas/dupr_explorer_may_2026";
const HF_CACHE_PATH = process.env.HF_CACHE_PATH || "player-cache.json";
const HF_TOKEN = process.env.HF_TOKEN || process.env.HUGGINGFACE_HUB_TOKEN;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootPath = path.resolve(__dirname, "..");
const distPath = path.resolve(rootPath, "dist");
const rosterPath = path.resolve(rootPath, "dupr_players.csv");
const cacheDir = path.resolve(rootPath, ".cache");
const cachePath = path.resolve(cacheDir, "player-cache.json");
let hydrateCachePromise = null;
let hfDatasetAccessPromise = null;
let lastCacheBackup = {
status: HF_TOKEN ? "pending" : "disabled",
message: HF_TOKEN ? "Waiting for cache changes." : "Set HF_TOKEN to enable Hugging Face dataset backup.",
fetchedAt: null,
pushedAt: null
};
const app = express();
app.use(express.json({ limit: "256kb" }));
function parseCsvLine(line) {
const cells = [];
let value = "";
let inQuotes = false;
for (let index = 0; index < line.length; index += 1) {
const character = line[index];
const nextCharacter = line[index + 1];
if (character === '"' && inQuotes && nextCharacter === '"') {
value += '"';
index += 1;
continue;
}
if (character === '"') {
inQuotes = !inQuotes;
continue;
}
if (character === "," && !inQuotes) {
cells.push(value.trim());
value = "";
continue;
}
value += character;
}
cells.push(value.trim());
return cells;
}
function parseCsv(text) {
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/).filter(Boolean);
const seenHeaders = {};
const headers = parseCsvLine(lines[0] || "").map((header) => {
const baseHeader = header.replace(/^\uFEFF/, "").trim();
seenHeaders[baseHeader] = (seenHeaders[baseHeader] ?? 0) + 1;
return seenHeaders[baseHeader] === 1 ? baseHeader : `${baseHeader}_${seenHeaders[baseHeader]}`;
});
return lines.slice(1).map((line) => {
const cells = parseCsvLine(line);
return Object.fromEntries(headers.map((header, index) => [header, cells[index] ?? ""]));
});
}
function normalizeStatus(value) {
return String(value || "").trim() || "Unknown";
}
function isValidDuprId(value) {
const normalized = String(value || "").trim();
return Boolean(normalized) && normalized.toUpperCase() !== "TBA";
}
function buildExplorerUrl(numericPlayerId) {
if (!looksLikeNumericPlayerId(numericPlayerId)) {
return null;
}
return EXPLORER_URL_PATTERN.replace("{numericPlayerId}", encodeURIComponent(numericPlayerId));
}
function normalizeRating(value) {
if (value === null || value === undefined || value === "") {
return null;
}
if (String(value).trim().toUpperCase() === "NR") {
return null;
}
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue.toFixed(3) : String(value);
}
function readPath(object, paths) {
for (const pathParts of paths) {
let value = object;
for (const part of pathParts) {
value = value && typeof value === "object" ? value[part] : undefined;
}
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
return null;
}
function extractRatingBasics(matchedPlayer) {
if (!matchedPlayer || typeof matchedPlayer !== "object") {
return {
singles: null,
doubles: null,
reliability: null,
matchCount: null
};
}
return {
singles: normalizeRating(
readPath(matchedPlayer, [
["singles"],
["singlesRating"],
["singlesDupr"],
["ratings", "singles"],
["rating", "singles"]
])
),
doubles: normalizeRating(
readPath(matchedPlayer, [
["doubles"],
["doublesRating"],
["doublesDupr"],
["ratings", "doubles"],
["rating", "doubles"]
])
),
reliability: readPath(matchedPlayer, [
["reliability"],
["reliabilityScore"],
["confidence"],
["confidenceScore"],
["ratingReliability"]
]),
matchCount: readPath(matchedPlayer, [
["matchCount"],
["matchesCount"],
["totalMatches"],
["stats", "matchCount"],
["stats", "totalMatches"]
])
};
}
function getDuprHeaders(token) {
return {
accept: "application/json",
authorization: `Bearer ${token}`,
"content-type": "application/json; charset=UTF-8",
origin: "https://dashboard.dupr.com",
referer: "https://dashboard.dupr.com/",
"user-agent": "Mozilla/5.0"
};
}
async function fetchDuprJson(url, options = {}) {
const token = process.env.DUPR_BEARER_TOKEN;
if (!token) {
throw apiError(500, "missing_token", "Missing DUPR_BEARER_TOKEN. Add it as a Hugging Face Space Secret.");
}
const response = await fetch(url, {
...options,
headers: {
...getDuprHeaders(token),
...(options.headers || {})
},
signal: AbortSignal.timeout(20000)
});
if (response.status === 401) {
throw apiError(401, "invalid_token", "DUPR token appears expired or invalid. Refresh the token.");
}
if (response.status === 429) {
throw apiError(429, "rate_limited", "DUPR rate limited the request. Try again later.");
}
if (!response.ok) {
throw apiError(response.status, "dupr_api_error", `DUPR API returned HTTP ${response.status}.`);
}
try {
return await response.json();
} catch {
throw apiError(502, "non_json_response", "DUPR returned a response that was not JSON.");
}
}
function summarizeMatchStats(statsData) {
const overview = statsData?.result?.overview || {};
const doubles = overview.doubles || {};
const all = overview.all || {};
return {
doubles: {
total: Number(doubles.all ?? 0),
wins: Number(doubles.wins ?? 0),
losses: Number(doubles.losses ?? 0),
pending: Number(doubles.pending ?? 0)
},
all: {
total: Number(all.all ?? 0),
wins: Number(all.wins ?? 0),
losses: Number(all.losses ?? 0),
pending: Number(all.pending ?? 0)
},
trends: statsData?.result?.trends ?? null
};
}
function sanitizeMatchInfo(matchInfo) {
if (!matchInfo || typeof matchInfo !== "object") {
return null;
}
return {
summary: matchInfo.summary ?? null,
recent: Array.isArray(matchInfo.recent) ? matchInfo.recent.slice(0, 10) : [],
totalHistory: Number(matchInfo.totalHistory ?? 0),
message: matchInfo.message ?? null,
errorCode: matchInfo.errorCode ?? null
};
}
async function fetchMatchInfo(numericPlayerId) {
const [statsData, historyData] = await Promise.all([
fetchDuprJson(`${DUPR_PLAYER_URL}/stats/${numericPlayerId}`),
fetchDuprJson(`${DUPR_PLAYER_URL}/${numericPlayerId}/history`, {
method: "POST",
body: JSON.stringify({ limit: 10, offset: 0 })
})
]);
const hits = Array.isArray(historyData?.result?.hits) ? historyData.result.hits : [];
return {
summary: summarizeMatchStats(statsData),
recent: hits.slice(0, 10),
totalHistory: Number(historyData?.result?.total ?? hits.length),
rawStats: statsData,
rawHistory: historyData
};
}
function getHfRepo() {
return {
type: "dataset",
name: HF_DATASET_REPO
};
}
function getHfErrorMessage(error) {
if (error?.statusCode === 404) {
return `Token cannot access dataset ${HF_DATASET_REPO}. Confirm the dataset repo exists, HF_DATASET_REPO matches it exactly, and HF_TOKEN has write access to that private dataset.`;
}
if (error?.statusCode === 401 || error?.statusCode === 403) {
return `HF_TOKEN was rejected for ${HF_DATASET_REPO}. Confirm the token is valid and has write access to the dataset.`;
}
return "Could not access the Hugging Face dataset; local cache will still be used.";
}
async function ensureHfDatasetAccess() {
if (!HF_TOKEN) {
return;
}
hfDatasetAccessPromise ??= checkRepoAccess({
repo: getHfRepo(),
accessToken: HF_TOKEN
});
await hfDatasetAccessPromise;
}
function updateCacheBackupStatus(status, message, extra = {}) {
lastCacheBackup = {
...lastCacheBackup,
status,
message,
...extra
};
}
async function hydrateCacheFromDataset() {
if (!HF_TOKEN) {
return;
}
try {
await fs.access(cachePath);
updateCacheBackupStatus("local", "Using local cache; dataset backup is available for future writes.");
return;
} catch (error) {
if (error.code !== "ENOENT") {
throw error;
}
}
try {
await ensureHfDatasetAccess();
const remoteFile = await downloadFile({
repo: getHfRepo(),
path: HF_CACHE_PATH,
accessToken: HF_TOKEN
});
if (!remoteFile) {
updateCacheBackupStatus("empty", `No cache backup found in ${HF_DATASET_REPO}/${HF_CACHE_PATH}.`);
return;
}
const text = await remoteFile.text();
const parsed = JSON.parse(text);
if (!parsed.players || typeof parsed.players !== "object") {
throw new Error("Remote cache backup does not contain a players object.");
}
await fs.mkdir(cacheDir, { recursive: true });
await fs.writeFile(cachePath, text.endsWith("\n") ? text : `${text}\n`);
updateCacheBackupStatus("hydrated", `Hydrated cache from ${HF_DATASET_REPO}/${HF_CACHE_PATH}.`, {
fetchedAt: new Date().toISOString()
});
} catch (error) {
console.error("Could not hydrate cache from Hugging Face dataset.", error);
updateCacheBackupStatus(
"hydrate_failed",
`${getHfErrorMessage(error)} Using local cache only.`
);
}
}
async function ensureCacheHydrated() {
hydrateCachePromise ??= hydrateCacheFromDataset();
await hydrateCachePromise;
}
async function readCache() {
await ensureCacheHydrated();
try {
const text = await fs.readFile(cachePath, "utf8");
const parsed = JSON.parse(text);
return {
players: parsed.players && typeof parsed.players === "object" ? parsed.players : {}
};
} catch (error) {
if (error.code === "ENOENT") {
return { players: {} };
}
throw error;
}
}
async function writeCache(cache) {
await fs.mkdir(cacheDir, { recursive: true });
const serialized = `${JSON.stringify(cache, null, 2)}\n`;
await fs.writeFile(cachePath, serialized);
if (!HF_TOKEN) {
return;
}
try {
await ensureHfDatasetAccess();
await uploadFile({
repo: getHfRepo(),
file: {
path: HF_CACHE_PATH,
content: new Blob([serialized], { type: "application/json" })
},
accessToken: HF_TOKEN,
commitTitle: "Update DUPR player cache backup"
});
updateCacheBackupStatus("pushed", `Backed up cache to ${HF_DATASET_REPO}/${HF_CACHE_PATH}.`, {
pushedAt: new Date().toISOString()
});
} catch (error) {
console.error("Could not back up cache to Hugging Face dataset.", error);
updateCacheBackupStatus(
"push_failed",
`${getHfErrorMessage(error)} Local cache was still written.`
);
}
}
function walkJson(value) {
if (Array.isArray(value)) {
return value.flatMap(walkJson);
}
if (value && typeof value === "object") {
return [value, ...Object.values(value).flatMap(walkJson)];
}
return [];
}
function looksLikeNumericPlayerId(value) {
return /^\d{8,}$/.test(String(value ?? ""));
}
function findPlayerId(data, query) {
const normalizedQuery = String(query ?? "").trim().toLowerCase();
const idFields = ["id", "playerId", "player_id", "userId", "user_id", "duprId", "dupr_id"];
const candidates = walkJson(data).filter((item) => {
try {
return normalizedQuery && JSON.stringify(item).toLowerCase().includes(normalizedQuery);
} catch {
return false;
}
});
for (const candidate of candidates) {
for (const field of idFields) {
if (looksLikeNumericPlayerId(candidate[field])) {
return {
numericPlayerId: String(candidate[field]),
matchedPlayer: candidate,
candidates
};
}
}
}
for (const candidate of candidates) {
for (const value of Object.values(candidate)) {
if (looksLikeNumericPlayerId(value)) {
return {
numericPlayerId: String(value),
matchedPlayer: candidate,
candidates
};
}
}
}
return {
numericPlayerId: null,
matchedPlayer: candidates[0] ?? null,
candidates
};
}
function apiError(status, errorCode, message) {
return {
status,
body: {
ok: false,
errorCode,
message
}
};
}
async function resolveDuprPlayer({
query,
lat = DEFAULT_LAT,
lng = DEFAULT_LNG,
includeUnclaimedPlayers = true
}) {
const normalizedQuery = String(query ?? "").trim();
const token = process.env.DUPR_BEARER_TOKEN;
if (!normalizedQuery) {
throw apiError(400, "empty_query", "Enter a DUPR ID or player code.");
}
if (!token) {
throw apiError(500, "missing_token", "Missing DUPR_BEARER_TOKEN. Add it as a Hugging Face Space Secret.");
}
const payload = {
limit: 10,
offset: 0,
query: normalizedQuery,
exclude: [],
includeUnclaimedPlayers,
filter: {
lat: Number.isFinite(lat) ? lat : DEFAULT_LAT,
lng: Number.isFinite(lng) ? lng : DEFAULT_LNG,
rating: {
maxRating: null,
minRating: null
},
locationText: ""
}
};
let response;
try {
response = await fetch(DUPR_SEARCH_URL, {
method: "POST",
headers: getDuprHeaders(token),
body: JSON.stringify(payload),
signal: AbortSignal.timeout(20000)
});
} catch (error) {
const message =
error?.name === "TimeoutError"
? "DUPR request timed out. Try again."
: "Could not reach DUPR. Check the network and try again.";
throw apiError(502, "network_error", message);
}
if (response.status === 401) {
throw apiError(401, "invalid_token", "DUPR token appears expired or invalid. Refresh the token.");
}
if (response.status === 429) {
throw apiError(429, "rate_limited", "DUPR rate limited the request. Try again later.");
}
if (!response.ok) {
throw apiError(response.status, "dupr_api_error", `DUPR API returned HTTP ${response.status}.`);
}
let data;
try {
data = await response.json();
} catch {
throw apiError(502, "non_json_response", "DUPR returned a response that was not JSON.");
}
const { numericPlayerId, matchedPlayer, candidates } = findPlayerId(data, normalizedQuery);
if (!numericPlayerId) {
const error = apiError(
404,
candidates.length ? "missing_numeric_id" : "no_match",
candidates.length
? "Found possible matches, but no numeric dashboard player id was obvious."
: "No matching player was found."
);
throw {
...error,
body: {
...error.body,
matchedPlayer,
raw: data
}
};
}
const dashboardUrl = `https://dashboard.dupr.com/dashboard/player/${numericPlayerId}`;
const profileUrl = `https://dashboard.dupr.com/dashboard/player/${numericPlayerId}/profile`;
return {
ok: true,
numericPlayerId,
dashboardUrl,
profileUrl,
matchedPlayer,
ratingBasics: extractRatingBasics(matchedPlayer),
raw: data
};
}
async function readRoster() {
const [text, cache] = await Promise.all([fs.readFile(rosterPath, "utf8"), readCache()]);
const rows = parseCsv(text);
const teams = rows.map((row, index) => {
const level = String(row.LEVEL || "").trim();
const status = normalizeStatus(row.Status);
const teamNumber = String(row.SN || index + 1).trim();
const players = [
{
slot: 1,
name: String(row["PLAYER 1"] || "").trim(),
duprId: String(row["DUPR ID"] || "").trim()
},
{
slot: 2,
name: String(row["PLAYER 2"] || "").trim(),
duprId: String(row["DUPR ID_2"] || row["DUPR ID.1"] || "").trim()
}
].map((player) => {
const validDuprId = isValidDuprId(player.duprId);
const cached = validDuprId ? cache.players[player.duprId] ?? null : null;
const numericPlayerId = cached?.numericPlayerId ?? null;
return {
...player,
validDuprId,
explorerUrl: buildExplorerUrl(numericPlayerId),
cache: cached
? {
status: cached.status,
fetchedAt: cached.fetchedAt,
numericPlayerId,
dashboardUrl: cached.dashboardUrl ?? null,
profileUrl: cached.profileUrl ?? null,
ratingBasics: cached.ratingBasics ?? null,
matchInfo: sanitizeMatchInfo(cached.matchInfo),
message: cached.message ?? null,
errorCode: cached.errorCode ?? null
}
: null
};
});
return {
id: `${teamNumber}-${index + 1}`,
rowIndex: index + 1,
teamNumber,
level,
status,
complete: players.every((player) => player.name && player.validDuprId),
players
};
});
const allPlayers = teams.flatMap((team) => team.players);
const validPlayers = allPlayers.filter((player) => player.validDuprId);
const enrichedPlayers = validPlayers.filter((player) => player.cache?.status === "success");
const attemptedPlayers = validPlayers.filter((player) => player.cache);
const countBy = (items, getter) =>
items.reduce((counts, item) => {
const key = getter(item);
counts[key] = (counts[key] ?? 0) + 1;
return counts;
}, {});
return {
teams,
stats: {
totalTeams: teams.length,
completeTeams: teams.filter((team) => team.complete).length,
incompleteTeams: teams.filter((team) => !team.complete).length,
totalPlayers: allPlayers.filter((player) => player.name).length,
validDuprIds: validPlayers.length,
enrichedPlayers: enrichedPlayers.length,
unresolvedPlayers: attemptedPlayers.filter((player) => player.cache?.status === "error").length,
levels: countBy(teams, (team) => team.level),
statuses: countBy(teams, (team) => team.status)
},
cache: {
cachedPlayers: attemptedPlayers.length,
explorerUrlPattern: EXPLORER_URL_PATTERN,
backup: {
repo: HF_DATASET_REPO,
path: HF_CACHE_PATH,
...lastCacheBackup
}
}
};
}
async function refreshPlayer(duprId) {
const normalizedDuprId = String(duprId || "").trim();
const cache = await readCache();
if (!isValidDuprId(normalizedDuprId)) {
const entry = {
status: "error",
fetchedAt: new Date().toISOString(),
errorCode: "invalid_dupr_id",
message: "This player does not have a valid DUPR ID."
};
cache.players[normalizedDuprId || "missing"] = entry;
await writeCache(cache);
return entry;
}
try {
const result = await resolveDuprPlayer({ query: normalizedDuprId });
const matchInfo = await fetchMatchInfo(result.numericPlayerId).catch((error) => ({
errorCode: error?.body?.errorCode || error?.errorCode || "match_info_failed",
message: error?.body?.message || error?.message || "Could not load match information.",
summary: null,
recent: [],
totalHistory: 0
}));
const entry = {
status: "success",
fetchedAt: new Date().toISOString(),
numericPlayerId: result.numericPlayerId,
dashboardUrl: result.dashboardUrl,
profileUrl: result.profileUrl,
matchedPlayer: result.matchedPlayer,
ratingBasics: result.ratingBasics,
matchInfo,
raw: result.raw
};
cache.players[normalizedDuprId] = entry;
await writeCache(cache);
return entry;
} catch (error) {
const existingEntry = cache.players[normalizedDuprId];
const errorCode = error?.body?.errorCode || error?.errorCode || "refresh_failed";
if (
existingEntry?.status === "success" &&
["invalid_token", "rate_limited", "network_error", "dupr_api_error"].includes(errorCode)
) {
return existingEntry;
}
const entry = {
status: "error",
fetchedAt: new Date().toISOString(),
errorCode,
message: error?.body?.message || error?.message || "Could not refresh this player.",
matchedPlayer: error?.body?.matchedPlayer ?? null,
raw: error?.body?.raw ?? null
};
cache.players[normalizedDuprId] = entry;
await writeCache(cache);
return entry;
}
}
app.get("/api/health", (_req, res) => {
res.json({ ok: true });
});
app.get("/api/roster", async (_req, res) => {
try {
res.json(await readRoster());
} catch (error) {
console.error("Could not read roster CSV.", error);
res.status(500).json({
ok: false,
errorCode: "roster_read_failed",
message: "Could not read the roster CSV."
});
}
});
app.get("/api/cache", async (_req, res) => {
try {
const cache = await readCache();
res.json({
players: Object.fromEntries(
Object.entries(cache.players).map(([duprId, entry]) => [
duprId,
{
status: entry.status,
fetchedAt: entry.fetchedAt,
numericPlayerId: entry.numericPlayerId ?? null,
message: entry.message ?? null
}
])
),
backup: {
repo: HF_DATASET_REPO,
path: HF_CACHE_PATH,
...lastCacheBackup
}
});
} catch {
res.status(500).json({
ok: false,
errorCode: "cache_read_failed",
message: "Could not read the player cache."
});
}
});
app.post("/api/players/:duprId/refresh", async (req, res) => {
const entry = await refreshPlayer(req.params.duprId);
res.status(entry.status === "success" ? 200 : 207).json(entry);
});
app.post("/api/players/refresh", async (req, res) => {
const roster = await readRoster();
const cache = await readCache();
const force = Boolean(req.body?.force);
const duprIds = [
...new Set(
roster.teams
.flatMap((team) => team.players)
.filter((player) => player.validDuprId)
.map((player) => player.duprId)
)
].filter((duprId) => force || cache.players[duprId]?.status !== "success" || !cache.players[duprId]?.matchInfo);
const results = [];
for (const duprId of duprIds) {
results.push({
duprId,
result: await refreshPlayer(duprId)
});
}
res.json({
ok: true,
refreshed: results.length,
results
});
});
app.post("/api/resolve", async (req, res) => {
try {
const result = await resolveDuprPlayer({
query: req.body?.query,
lat: Number(req.body?.lat ?? DEFAULT_LAT),
lng: Number(req.body?.lng ?? DEFAULT_LNG),
includeUnclaimedPlayers: Boolean(req.body?.includeUnclaimedPlayers ?? true)
});
res.json(result);
} catch (error) {
const status = error?.status || 500;
const body = error?.body || {
ok: false,
errorCode: "resolve_failed",
message: "The resolver could not complete the request. Try again."
};
res.status(status).json(body);
}
});
if (process.env.NODE_ENV === "production") {
app.use(express.static(distPath));
app.get("*", (_req, res) => {
res.sendFile(path.join(distPath, "index.html"));
});
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`DUPR roster dashboard listening on port ${PORT}`);
});