import { createBeatSaverAPI, type PlaylistFull, type PlaylistSearchParams, type PlaylistSearchResponse } from '$lib/server/beatsaver'; import type { PageServerLoad } from './$types'; function parsePositiveInt(value: string | null, fallback: number): number { if (!value) return fallback; const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed <= 0) return fallback; return parsed; } const SORT_ORDER_VALUES = ['Latest', 'Relevance', 'Curated'] as const; type SortOrderOption = (typeof SORT_ORDER_VALUES)[number]; const SORT_ORDER_SET = new Set(SORT_ORDER_VALUES); const DEFAULT_SORT_ORDER: SortOrderOption = 'Latest'; function parseSortOrder(value: string | null): SortOrderOption { if (!value) return DEFAULT_SORT_ORDER; const normalized = value.trim(); return SORT_ORDER_SET.has(normalized) ? (normalized as SortOrderOption) : DEFAULT_SORT_ORDER; } type SearchInfoShape = { page?: number; pages?: number; total?: number; size?: number; itemsPerPage?: number; }; export const load: PageServerLoad = async ({ url, fetch, parent }) => { const parentData = await parent(); const requestedPage = parsePositiveInt(url.searchParams.get('page'), 1); const pageIndex = requestedPage - 1; const api = createBeatSaverAPI(fetch); const MIN_MAPS = 3; const hasSubmitted = url.searchParams.has('submitted'); const rawQuery = url.searchParams.get('q') ?? ''; const query = rawQuery.trim(); const rawSortOrder = url.searchParams.get('order') ?? url.searchParams.get('sortOrder'); const sortOrder = parseSortOrder(rawSortOrder); const curated = url.searchParams.has('curated'); const verified = url.searchParams.has('verified') ? true : hasSubmitted ? false : true; const searchState = { submitted: hasSubmitted, query, sortOrder, curated, verified }; const searchParams: PlaylistSearchParams = { page: pageIndex, sortOrder, includeEmpty: false }; if (query) { searchParams.query = query; } if (curated) { searchParams.curated = true; } if (verified) { searchParams.verified = true; } try { const response = (await api.searchPlaylists({ ...searchParams })) as PlaylistSearchResponse; const docsRaw = Array.isArray(response?.docs) ? response.docs : []; const docs = docsRaw.filter((playlist) => { const totalMaps = playlist?.stats?.totalMaps ?? 0; return Number.isFinite(totalMaps) && totalMaps >= MIN_MAPS; }); const info = (response?.info && typeof response.info === 'object' ? (response.info as SearchInfoShape) : null) ?? null; const infoPage = typeof info?.page === 'number' ? info.page : pageIndex; const currentPage = infoPage + 1; const totalPages = typeof info?.pages === 'number' ? info.pages : null; const total = typeof info?.total === 'number' ? info.total : null; const pageSize = typeof info?.size === 'number' ? info.size : typeof info?.itemsPerPage === 'number' ? info.itemsPerPage : docs.length; return { ...parentData, playlists: docs, info, page: currentPage, totalPages, total, pageSize, error: null, search: searchState }; } catch (err) { console.error('Failed to load BeatSaver playlists', err); return { ...parentData, playlists: [] as PlaylistFull[], info: null, page: requestedPage, totalPages: null, total: null, pageSize: 0, error: err instanceof Error ? err.message : 'Failed to load playlists', search: searchState }; } };