feat: backfill mappool slot metadata and user/team avatars to R2, improving caching and reducing API calls

This commit is contained in:
2026-03-01 17:21:02 +00:00
parent f080ceaf87
commit 2949d251ee
4 changed files with 267 additions and 71 deletions

View File

@@ -21,11 +21,47 @@ export const load: PageServerLoad = async ({ params, locals }) => {
if (!pool) error(404, 'Mappool not found');
// Fetch beatmap metadata for all slots
// Use cached metadata from DB — only fall back to osu! API for legacy slots
const slotsWithBeatmaps = await Promise.all(
pool.slots.map(async (slot) => {
// Fast path: metadata already stored in DB (R2 URLs)
if (slot.title && slot.coverUrl) {
return {
...slot,
beatmap: {
title: slot.title,
artist: slot.artist ?? '',
version: slot.version ?? '',
starRating: slot.starRating ?? 0,
bpm: slot.bpm ?? 0,
totalLength: slot.totalLength ?? 0,
coverUrl: slot.coverUrl,
url: `https://osu.ppy.sh/beatmaps/${slot.beatmapId}`
}
};
}
// Slow path: legacy slot without cached metadata — fetch + backfill
try {
const beatmap = await getBeatmap(slot.beatmapId);
const coverUrl = await proxyImage(beatmap.beatmapset.covers['card@2x']);
const listCoverUrl = await proxyImage(beatmap.beatmapset.covers['list@2x']);
// Backfill the slot so future loads are instant
await db
.update(mappoolSlot)
.set({
title: beatmap.beatmapset.title,
artist: beatmap.beatmapset.artist,
version: beatmap.version,
coverUrl,
listCoverUrl,
starRating: beatmap.difficulty_rating,
bpm: beatmap.bpm,
totalLength: beatmap.total_length
})
.where(eq(mappoolSlot.id, slot.id));
return {
...slot,
beatmap: {
@@ -35,7 +71,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
starRating: beatmap.difficulty_rating,
bpm: beatmap.bpm,
totalLength: beatmap.total_length,
coverUrl: await proxyImage(beatmap.beatmapset.covers['card@2x']),
coverUrl,
url: beatmap.url
}
};
@@ -105,6 +141,16 @@ export const actions: Actions = {
where: and(eq(mappoolSlot.mappoolId, params.id), eq(mappoolSlot.category, category))
});
// Proxy covers to R2 at insert time — pages never hit osu! again
let coverUrl: string | null = null;
let listCoverUrl: string | null = null;
try {
coverUrl = await proxyImage(beatmap.beatmapset.covers['card@2x']);
listCoverUrl = await proxyImage(beatmap.beatmapset.covers['list@2x']);
} catch (err: any) {
console.warn('[Mappool] Failed to proxy covers to R2:', err.message);
}
await db.insert(mappoolSlot).values({
mappoolId: params.id,
beatmapId,
@@ -113,7 +159,13 @@ export const actions: Actions = {
starRating: beatmap.difficulty_rating,
bpm: beatmap.bpm,
totalLength: beatmap.total_length,
mods: category === 'NM' || category === 'TB' || category === 'FM' ? [] : [category]
mods: category === 'NM' || category === 'TB' || category === 'FM' ? [] : [category],
// Cached metadata — all R2 URLs
title: beatmap.beatmapset.title,
artist: beatmap.beatmapset.artist,
version: beatmap.version,
coverUrl,
listCoverUrl
});
return { success: true };

View File

@@ -1,5 +1,6 @@
import { user } from '$lib/server/db/schema';
import { db } from '$lib/server/db';
import { mappoolSlot } from '$lib/server/db/schema';
import { getMatchFull, submitRoll, pickMap, cancelMatch } from '$lib/server/match/engine';
import { playPickedMap, closeLobby, forceStartGame } from '$lib/server/match/orchestrator';
import { getLobby, getLobbyStatus } from '$lib/server/bancho/client';
@@ -22,13 +23,32 @@ export const load: PageServerLoad = async ({ params, locals }) => {
error(404, 'Match not found');
}
// Build beatmap metadata cache for all slots in the mappool
// Build beatmap metadata cache — prefer DB-stored R2 URLs, fall back to API
const beatmapCache: Record<string, any> = {};
if (m.mappool?.slots) {
await Promise.allSettled(
m.mappool.slots.map(async (slot) => {
// Fast path: metadata already stored in DB
if (slot.title && slot.coverUrl) {
beatmapCache[slot.beatmapId] = {
title: slot.title,
artist: slot.artist ?? '',
version: slot.version ?? '',
starRating: slot.starRating ?? 0,
bpm: slot.bpm ?? 0,
totalLength: slot.totalLength ?? 0,
coverUrl: slot.coverUrl,
listCoverUrl: slot.listCoverUrl
};
return;
}
// Slow path: legacy slot — fetch from API + backfill DB
try {
const bm = await getBeatmap(slot.beatmapId);
const coverUrl = await proxyImage(bm.beatmapset.covers['card@2x']);
const listCoverUrl = await proxyImage(bm.beatmapset.covers['list@2x']);
beatmapCache[slot.beatmapId] = {
title: bm.beatmapset.title,
artist: bm.beatmapset.artist,
@@ -36,9 +56,24 @@ export const load: PageServerLoad = async ({ params, locals }) => {
starRating: bm.difficulty_rating,
bpm: bm.bpm,
totalLength: bm.total_length,
coverUrl: await proxyImage(bm.beatmapset.covers['card@2x']),
listCoverUrl: await proxyImage(bm.beatmapset.covers['list@2x'])
coverUrl,
listCoverUrl
};
// Backfill so future loads are instant
await db
.update(mappoolSlot)
.set({
title: bm.beatmapset.title,
artist: bm.beatmapset.artist,
version: bm.version,
coverUrl,
listCoverUrl,
starRating: bm.difficulty_rating,
bpm: bm.bpm,
totalLength: bm.total_length
})
.where(eq(mappoolSlot.id, slot.id));
} catch { /* skip */ }
})
);