104 lines
2.9 KiB
TypeScript
104 lines
2.9 KiB
TypeScript
import { z } from "zod";
|
|
// Import client-db index directly as requested.
|
|
// On the server, `db` will be null (because `browser` is false in that module).
|
|
import { db, ensureSeeded, getSongsForMalAnimeIds } from "$lib/db/client-db";
|
|
import {
|
|
MalAnimeListQuerySchema,
|
|
MalAnimeListResponseSchema,
|
|
MalAnimeListStatusEnum,
|
|
} from "$lib/types/mal";
|
|
import { songTypesCodec } from "../songs/schema";
|
|
import type { PageLoad } from "./$types";
|
|
|
|
const LIST_QUERY_LIMIT = 1000;
|
|
|
|
const SearchSchema = MalAnimeListQuerySchema.extend({
|
|
// Username
|
|
mal: z.string().optional(),
|
|
|
|
// Allow empty string to mean "All"
|
|
status: MalAnimeListStatusEnum.or(z.literal("")).optional(),
|
|
type: songTypesCodec.optional(),
|
|
}).strict();
|
|
|
|
type StatusParam = z.infer<typeof SearchSchema>["status"];
|
|
function normalizeStatus(
|
|
status: StatusParam,
|
|
): z.infer<typeof MalAnimeListStatusEnum> | undefined {
|
|
if (status == null || status === "") return undefined;
|
|
return status;
|
|
}
|
|
|
|
export const load: PageLoad = async ({ url, fetch, depends }) => {
|
|
depends("mal:animelist");
|
|
depends("clientdb:songs");
|
|
|
|
const parsed = SearchSchema.safeParse(
|
|
Object.fromEntries(url.searchParams.entries()),
|
|
);
|
|
|
|
const mal = parsed.success ? parsed.data.mal : undefined;
|
|
const status = parsed.success
|
|
? normalizeStatus(parsed.data.status)
|
|
: undefined;
|
|
const types = parsed.success ? parsed.data.type : undefined;
|
|
|
|
const username = (mal ?? "").trim();
|
|
|
|
// Always return a stable shape for hydration
|
|
if (!username) {
|
|
return {
|
|
LIST_QUERY_LIMIT,
|
|
username: "",
|
|
status: status ?? null,
|
|
malResponse: null as z.infer<typeof MalAnimeListResponseSchema> | null,
|
|
songRows: [] as Awaited<ReturnType<typeof getSongsForMalAnimeIds>>,
|
|
};
|
|
}
|
|
|
|
// This endpoint proxies MAL and works server-side.
|
|
const malUrl = new URL(
|
|
`/api/mal/animelist/${encodeURIComponent(username)}`,
|
|
url.origin,
|
|
);
|
|
|
|
malUrl.searchParams.set("limit", String(LIST_QUERY_LIMIT));
|
|
if (status) malUrl.searchParams.set("status", status);
|
|
|
|
// NOTE: If you later want to support sort/offset, add them here from SearchSchema too.
|
|
const malRes = await fetch(malUrl);
|
|
|
|
if (!malRes.ok) {
|
|
// Let +page.svelte decide how to display errors; throw to use SvelteKit error page
|
|
throw new Error(`MAL request failed (${malRes.status})`);
|
|
}
|
|
|
|
const malJson: unknown = await malRes.json();
|
|
const malResponse = MalAnimeListResponseSchema.parse(malJson);
|
|
|
|
// Client-only DB: on the server `db` is null, so return [] and let hydration re-run load in browser.
|
|
if (!db) {
|
|
return {
|
|
LIST_QUERY_LIMIT,
|
|
username,
|
|
status: status ?? null,
|
|
malResponse,
|
|
songRows: [] as Awaited<ReturnType<typeof getSongsForMalAnimeIds>>,
|
|
};
|
|
}
|
|
|
|
// Browser path: seed then query local DB for songs by MAL ids
|
|
await ensureSeeded({ fetch });
|
|
|
|
const malIds = malResponse.data.map((e) => e.node.id);
|
|
const songRows = await getSongsForMalAnimeIds(db, malIds, types);
|
|
|
|
return {
|
|
LIST_QUERY_LIMIT,
|
|
username,
|
|
status: status ?? null,
|
|
malResponse,
|
|
songRows,
|
|
};
|
|
};
|