add osu API

This commit is contained in:
2026-02-28 01:17:52 +01:00
parent 191ec08f7f
commit 290b9752a9
2 changed files with 133 additions and 0 deletions

119
src/lib/server/osu/api.ts Normal file
View File

@@ -0,0 +1,119 @@
import { env } from '$env/dynamic/private';
import { db } from '$lib/server/db';
import { and, eq } from 'drizzle-orm';
import { account } from '$lib/server/db/auth.schema';
let clientToken: string | null = null;
let clientTokenExpiresAt = 0;
// Client credentials token (no user needed, for public endpoints)
async function getClientToken(): Promise<string> {
if (clientToken && Date.now() < clientTokenExpiresAt - 60_000) {
return clientToken;
}
const res = await fetch('https://osu.ppy.sh/oauth/token', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
client_id: env.OSU_CLIENT_ID!,
client_secret: env.OSU_CLIENT_SECRET!,
grant_type: 'client_credentials',
scope: 'identify public'
})
});
if (!res.ok) {
throw new Error(`Failed to get osu! client token: ${res.status} ${res.statusText}`);
}
const data = await res.json();
clientToken = data.access_token;
clientTokenExpiresAt = Date.now() + data.expires_in * 1000;
return clientToken!;
}
// Get a user's access token (for user-scoped endpoints)
async function getUserToken(userId: string): Promise<string | null> {
const acc = await db.query.account.findFirst({
where: and(eq(account.userId, userId), eq(account.providerId, 'osu'))
});
if (!acc?.accessToken) return null;
// Check if expired and refresh if needed
if (acc.accessTokenExpiresAt && acc.accessTokenExpiresAt < new Date()) {
if (!acc.refreshToken) return null;
const res = await fetch('https://osu.ppy.sh/oauth/token', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
client_id: env.OSU_CLIENT_ID!,
client_secret: env.OSU_CLIENT_SECRET!,
grant_type: 'refresh_token',
refresh_token: acc.refreshToken,
scope: 'identify public'
})
});
if (!res.ok) return null;
const data = await res.json();
await db
.update(account)
.set({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessTokenExpiresAt: new Date(Date.now() + data.expires_in * 1000)
})
.where(eq(account.id, acc.id));
return data.access_token;
}
return acc.accessToken;
}
async function osuFetch(path: string, token?: string) {
const t = token ?? (await getClientToken());
const res = await fetch(`https://osu.ppy.sh/api/v2${path}`, {
headers: { Authorization: `Bearer ${t}` }
});
if (!res.ok) {
throw new Error(`osu! API ${path}: ${res.status} ${res.statusText}`);
}
return res.json();
}
// ── Public API ──────────────────────────────────────────────────────────
export async function getBeatmap(beatmapId: string | number) {
return osuFetch(`/beatmaps/${beatmapId}`);
}
export async function getBeatmapset(beatmapsetId: string | number) {
return osuFetch(`/beatmapsets/${beatmapsetId}`);
}
export async function getUser(userId: string | number) {
return osuFetch(`/users/${userId}`);
}
export async function getUserScores(
userId: string | number,
type: 'best' | 'recent' | 'firsts' = 'best',
limit = 10
) {
return osuFetch(`/users/${userId}/scores/${type}?limit=${limit}`);
}
export { getUserToken, getClientToken };