refactor song type schema on search page

This commit is contained in:
2026-02-09 20:49:47 -08:00
parent 2bf5aeb1c0
commit 72ee0260d9
4 changed files with 62 additions and 37 deletions

View File

@@ -2,18 +2,28 @@ import { z } from "zod";
import { AmqAnimeCategory, AmqAnimeGenre, AmqAnimeTag } from "./anime-extended"; import { AmqAnimeCategory, AmqAnimeGenre, AmqAnimeTag } from "./anime-extended";
export const Season = z.enum({ export const AmqSeasonMap = {
Winter: 0, Winter: 0,
Spring: 1, Spring: 1,
Summer: 2, Summer: 2,
Fall: 3, Fall: 3,
} as const); } as const;
export const SongLinkType = z.enum({ export const AmqSeason = z.enum(AmqSeasonMap);
export const AmqSongLinkTypeMap = {
OP: 1, OP: 1,
ED: 2, ED: 2,
INS: 3, INS: 3,
} as const); } as const;
export const AmqSongLinkTypeMapReverse = {
1: "OP",
2: "ED",
3: "INS",
} as const;
export const AmqSongLinkType = z.enum(AmqSongLinkTypeMap);
const BooleanInt = z.enum({ const BooleanInt = z.enum({
false: 0, false: 0,
@@ -23,7 +33,7 @@ const BooleanInt = z.enum({
export const AmqSongLink = z.object({ export const AmqSongLink = z.object({
songId: z.int().positive(), songId: z.int().positive(),
number: z.int().nonnegative(), number: z.int().nonnegative(),
type: SongLinkType, type: AmqSongLinkType,
annSongId: z.int().positive(), annSongId: z.int().positive(),
uploaded: BooleanInt, uploaded: BooleanInt,
rebroadcast: BooleanInt, rebroadcast: BooleanInt,
@@ -53,7 +63,7 @@ export const AmqAnimeSchema = z.object({
}), }),
), ),
year: z.int().positive(), year: z.int().positive(),
seasonId: Season, seasonId: AmqSeason,
songLinks: z.array(AmqSongLink), songLinks: z.array(AmqSongLink),
opCount: z.int().nonnegative(), opCount: z.int().nonnegative(),
edCount: z.int().nonnegative(), edCount: z.int().nonnegative(),

View File

@@ -7,17 +7,14 @@
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input"; import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label"; import { Label } from "$lib/components/ui/label";
import {
NativeSelect,
NativeSelectOption,
} from "$lib/components/ui/native-select";
import { db as clientDb } from "$lib/db/client-db"; import { db as clientDb } from "$lib/db/client-db";
import { addAllToQueue, playAllNext } from "$lib/player/player.svelte"; import { addAllToQueue, playAllNext } from "$lib/player/player.svelte";
import { trackFromSongRow } from "$lib/player/types"; import { trackFromSongRow } from "$lib/player/types";
import { AmqSongLinkTypeMap } from "$lib/types/amq";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { SearchParamsSchemaClient } from "./schema"; import { SearchParamsSchema } from "./schema";
const params = useSearchParams(SearchParamsSchemaClient, { const params = useSearchParams(SearchParamsSchema, {
pushHistory: false, pushHistory: false,
showDefaults: false, showDefaults: false,
}); });
@@ -123,13 +120,20 @@
</div> </div>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<Label for="song-type">Song Type</Label> <Label for="song-types">Song Type</Label>
<NativeSelect id="song-type" bind:value={params.songType}> <fieldset id="song-types">
<NativeSelectOption value="0">All</NativeSelectOption> {#each Object.keys(AmqSongLinkTypeMap) as type}
<NativeSelectOption value="1">OP</NativeSelectOption> <label for="song-type-{type.toLowerCase()}">{type}</label>
<NativeSelectOption value="2">ED</NativeSelectOption> <input
<NativeSelectOption value="3">INS</NativeSelectOption> type="checkbox"
</NativeSelect> bind:group={params.type}
value={AmqSongLinkTypeMap[
type as keyof typeof AmqSongLinkTypeMap
]}
id="song-type-{type.toLowerCase()}"
/>
{/each}
</fieldset>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<Label for="songs-limit">Limit</Label> <Label for="songs-limit">Limit</Label>
@@ -140,7 +144,7 @@
max="200" max="200"
step="20" step="20"
class="w-1/2" class="w-1/2"
bind:value={params.songsLimit} bind:value={params.limit}
/> />
</div> </div>
</div> </div>

View File

@@ -1,12 +1,12 @@
import type { SongFilters } from "$lib/db/client-db"; import type { SongFilters } from "$lib/db/client-db";
import { db, ensureSeeded, getSongsWithFilters } from "$lib/db/client-db"; import { db, ensureSeeded, getSongsWithFilters } from "$lib/db/client-db";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
import { SearchParamsSchemaServer } from "./schema"; import { SearchParamsSchema } from "./schema";
export const load: PageLoad = async ({ url, fetch, depends }) => { export const load: PageLoad = async ({ url, fetch, depends }) => {
depends("clientdb:songs"); depends("clientdb:songs");
const parsed = SearchParamsSchemaServer.safeParse( const parsed = SearchParamsSchema.safeParse(
Object.fromEntries(url.searchParams.entries()), Object.fromEntries(url.searchParams.entries()),
); );
@@ -19,7 +19,7 @@ export const load: PageLoad = async ({ url, fetch, depends }) => {
filters.globalPercentMin = parsed.data.gpm; filters.globalPercentMin = parsed.data.gpm;
if (parsed.data.gpx !== undefined) if (parsed.data.gpx !== undefined)
filters.globalPercentMax = parsed.data.gpx; filters.globalPercentMax = parsed.data.gpx;
if (parsed.data.songType) filters.songTypes = [parsed.data.songType]; if (parsed.data.type) filters.songTypes = parsed.data.type;
} }
// Client-only DB: on the server `db` is null, so return [] and let hydration re-run load in browser. // Client-only DB: on the server `db` is null, so return [] and let hydration re-run load in browser.
@@ -32,11 +32,7 @@ export const load: PageLoad = async ({ url, fetch, depends }) => {
await ensureSeeded({ fetch }); await ensureSeeded({ fetch });
const songRows = await getSongsWithFilters( const songRows = await getSongsWithFilters(db, filters, parsed.data?.limit);
db,
filters,
parsed.data?.songsLimit,
);
return { return {
filters: parsed.success ? parsed.data : {}, // Return original parsed data for form state filters: parsed.success ? parsed.data : {}, // Return original parsed data for form state

View File

@@ -1,18 +1,33 @@
import { z } from "zod"; import { z } from "zod";
import {
AmqSongLinkType,
AmqSongLinkTypeMap,
AmqSongLinkTypeMapReverse,
} from "$lib/types/amq";
const SEP = ",";
const songTypesCodec = z.codec(z.string(), z.array(AmqSongLinkType), {
decode: (str) =>
str
? decodeURIComponent(str)
.split(SEP)
.map((s) => AmqSongLinkTypeMap[s as keyof typeof AmqSongLinkTypeMap])
: [],
encode: (arr) =>
arr
? encodeURIComponent(
arr.map((a) => AmqSongLinkTypeMapReverse[a]).join(SEP),
)
: "",
});
export const SearchParamsSchema = z.object({ export const SearchParamsSchema = z.object({
songsLimit: z.coerce.number().int().default(20), limit: z.coerce.number().int().default(20),
song: z.string().optional().default(""), song: z.string().optional().default(""),
artist: z.string().optional().default(""), artist: z.string().optional().default(""),
anime: z.string().optional().default(""), anime: z.string().optional().default(""),
gpm: z.coerce.number().int().optional().default(0), gpm: z.coerce.number().int().optional().default(0),
gpx: z.coerce.number().int().optional().default(100), gpx: z.coerce.number().int().optional().default(100),
}); type: songTypesCodec.default([]),
export const SearchParamsSchemaClient = SearchParamsSchema.extend({
songType: z.string().optional().default("0"),
});
export const SearchParamsSchemaServer = SearchParamsSchema.extend({
songType: z.coerce.number().int().optional(),
}); });