feat: backfill mappool slot metadata and user/team avatars to R2, improving caching and reducing API calls
This commit is contained in:
@@ -1,8 +1,3 @@
|
||||
// src/lib/server/db/schema.ts
|
||||
//
|
||||
// CHANGE: Add `role` column to user table in auth.schema.ts (see that file).
|
||||
// This file adds the permission helper utilities.
|
||||
|
||||
import {
|
||||
pgTable,
|
||||
integer,
|
||||
@@ -60,7 +55,13 @@ export const mappoolSlot = pgTable('mappool_slot', {
|
||||
starRating: real('star_rating'),
|
||||
bpm: real('bpm'),
|
||||
totalLength: integer('total_length'),
|
||||
mods: text('mods').array().default([]).notNull()
|
||||
mods: text('mods').array().default([]).notNull(),
|
||||
// ── Cached beatmap metadata (stored at insert time → zero API calls on page load) ──
|
||||
title: text('title'),
|
||||
artist: text('artist'),
|
||||
version: text('version'),
|
||||
coverUrl: text('cover_url'),
|
||||
listCoverUrl: text('list_cover_url')
|
||||
});
|
||||
|
||||
// ── Matches ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 */ }
|
||||
})
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user