123 lines
3.6 KiB
TypeScript
123 lines
3.6 KiB
TypeScript
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<string>(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<PlaylistFull>;
|
|
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
|
|
};
|
|
}
|
|
};
|
|
|
|
|