feat: enhance image URL handling; sanitize fallback URLs and improve cover image display logic

This commit is contained in:
2026-03-01 19:26:34 +01:00
parent 3ac16be68f
commit f399f1d546
4 changed files with 17 additions and 10 deletions

View File

@@ -38,10 +38,10 @@ const knownKeys = new Set<string>();
/** /**
* Returns a CDN URL for the given osu! image URL, uploading to R2 on first access. * Returns a CDN URL for the given osu! image URL, uploading to R2 on first access.
* Falls back to the original URL if R2 is not configured or the upload fails. * Returns null if R2 is not configured or the upload/fetch fails.
*/ */
export async function proxyImage(osuUrl: string): Promise<string> { export async function proxyImage(osuUrl: string): Promise<string | null> {
if (!isConfigured()) return osuUrl; if (!isConfigured()) return null;
const key = urlToKey(osuUrl); const key = urlToKey(osuUrl);
const cdnUrl = `${env.R2_PUBLIC_URL}/${key}`; const cdnUrl = `${env.R2_PUBLIC_URL}/${key}`;
@@ -65,6 +65,6 @@ export async function proxyImage(osuUrl: string): Promise<string> {
return cdnUrl; return cdnUrl;
} catch (err: unknown) { } catch (err: unknown) {
console.error('[R2] proxyImage failed for', key, '—', err instanceof Error ? err.message : err); console.error('[R2] proxyImage failed for', key, '—', err instanceof Error ? err.message : err);
return osuUrl; return null;
} }
} }

View File

@@ -24,8 +24,10 @@ export const load: PageServerLoad = async ({ params, locals }) => {
// Use cached metadata from DB — only fall back to osu! API for legacy slots // Use cached metadata from DB — only fall back to osu! API for legacy slots
const slotsWithBeatmaps = await Promise.all( const slotsWithBeatmaps = await Promise.all(
pool.slots.map(async (slot) => { pool.slots.map(async (slot) => {
// Fast path: metadata already stored in DB (R2 URLs) // Fast path: metadata already stored in DB
if (slot.title && slot.coverUrl) { if (slot.title !== null) {
// Sanitize any stale ppy.sh fallback URLs (image was unavailable when first fetched)
const coverUrl = slot.coverUrl?.includes('ppy.sh') ? null : (slot.coverUrl ?? null);
return { return {
...slot, ...slot,
beatmap: { beatmap: {
@@ -35,7 +37,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
starRating: slot.starRating ?? 0, starRating: slot.starRating ?? 0,
bpm: slot.bpm ?? 0, bpm: slot.bpm ?? 0,
totalLength: slot.totalLength ?? 0, totalLength: slot.totalLength ?? 0,
coverUrl: slot.coverUrl, coverUrl,
url: `https://osu.ppy.sh/beatmaps/${slot.beatmapId}` url: `https://osu.ppy.sh/beatmaps/${slot.beatmapId}`
} }
}; };

View File

@@ -30,7 +30,10 @@ export const load: PageServerLoad = async ({ params, locals }) => {
await Promise.allSettled( await Promise.allSettled(
m.mappool.slots.map(async (slot) => { m.mappool.slots.map(async (slot) => {
// Fast path: metadata already stored in DB // Fast path: metadata already stored in DB
if (slot.title && slot.coverUrl) { if (slot.title !== null) {
// Sanitize any stale ppy.sh fallback URLs (image was unavailable when first fetched)
const coverUrl = slot.coverUrl?.includes('ppy.sh') ? null : (slot.coverUrl ?? null);
const listCoverUrl = slot.listCoverUrl?.includes('ppy.sh') ? null : (slot.listCoverUrl ?? null);
beatmapCache[slot.beatmapId] = { beatmapCache[slot.beatmapId] = {
title: slot.title, title: slot.title,
artist: slot.artist ?? '', artist: slot.artist ?? '',
@@ -38,8 +41,8 @@ export const load: PageServerLoad = async ({ params, locals }) => {
starRating: slot.starRating ?? 0, starRating: slot.starRating ?? 0,
bpm: slot.bpm ?? 0, bpm: slot.bpm ?? 0,
totalLength: slot.totalLength ?? 0, totalLength: slot.totalLength ?? 0,
coverUrl: slot.coverUrl, coverUrl,
listCoverUrl: slot.listCoverUrl listCoverUrl
}; };
return; return;
} }

View File

@@ -502,6 +502,8 @@
<span class="w-6 text-center text-xs font-600 text-text-secondary">{game.gameNumber}</span> <span class="w-6 text-center text-xs font-600 text-text-secondary">{game.gameNumber}</span>
{#if bm?.listCoverUrl} {#if bm?.listCoverUrl}
<img src={bm.listCoverUrl} alt="" class="h-8 w-16 rounded object-cover" /> <img src={bm.listCoverUrl} alt="" class="h-8 w-16 rounded object-cover" />
{:else}
<div class="h-8 w-16 flex-shrink-0 rounded bg-surface-700"></div>
{/if} {/if}
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
{#if bm} {#if bm}