1 Commits

Author SHA1 Message Date
775b67c177 ui: list: add song type filter 2026-03-27 03:14:19 -07:00
4 changed files with 29 additions and 3 deletions

View File

@@ -161,6 +161,7 @@ export async function getAnimeWithSongsByAnnId(db: ClientDb, annId: number) {
export async function getSongsForMalAnimeIds(
db: ClientDb,
malAnimeIds: number[],
songTypes?: number[],
) {
const ids = malAnimeIds.filter((n) => Number.isFinite(n));
if (ids.length === 0) return [];
@@ -192,7 +193,14 @@ export async function getSongsForMalAnimeIds(
.innerJoin(songs, eq(songs.annSongId, animeSongLinks.annSongId))
.leftJoin(artists, eq(artists.songArtistId, songs.songArtistId))
.leftJoin(groups, eq(groups.songGroupId, songs.songGroupId))
.where(inArray(anime.malId, ids))
.where(
songTypes && songTypes.length > 0
? and(
inArray(anime.malId, ids),
inArray(animeSongLinks.type, songTypes),
)
: inArray(anime.malId, ids),
)
.orderBy(
desc(anime.year),
desc(anime.seasonId),

View File

@@ -5,13 +5,16 @@
import { browser } from "$app/environment";
import { invalidate } from "$app/navigation";
import SongEntry from "$lib/components/SongEntry.svelte";
import { ChipGroup } from "$lib/components/ui/chip-group";
import { db as clientDb } from "$lib/db/client-db";
import { player } from "$lib/player/store.svelte";
import { trackFromSongRow } from "$lib/player/types";
import { AmqSongLinkTypeMap } from "$lib/types/amq";
import {
MalAnimeListQuerySchema,
MalAnimeListStatusEnum,
} from "$lib/types/mal";
import { songTypesCodec } from "../songs/schema";
import type { PageData } from "./$types";
const ListSearchSchema = MalAnimeListQuerySchema.extend({
@@ -19,6 +22,7 @@
status: MalAnimeListStatusEnum.or(z.literal("")).default(""),
// URL param `mal` is updated only on Search
mal: z.string().default(""),
type: songTypesCodec.default([]),
}).strict();
const params = useSearchParams(ListSearchSchema, {
@@ -130,6 +134,17 @@
</div>
</div>
<div class="mt-2">
<ChipGroup
label="Song Type"
items={Object.keys(AmqSongLinkTypeMap).map((type) => ({
label: type,
value: AmqSongLinkTypeMap[type as keyof typeof AmqSongLinkTypeMap],
}))}
bind:value={params.type}
/>
</div>
<div class="text-sm text-muted-foreground">
{#if data.username}
MAL entries: {data.malResponse?.data.length ?? 0} (limited to {data.LIST_QUERY_LIMIT})

View File

@@ -7,6 +7,7 @@ import {
MalAnimeListResponseSchema,
MalAnimeListStatusEnum,
} from "$lib/types/mal";
import { songTypesCodec } from "../songs/schema";
import type { PageLoad } from "./$types";
const LIST_QUERY_LIMIT = 1000;
@@ -17,6 +18,7 @@ const SearchSchema = MalAnimeListQuerySchema.extend({
// Allow empty string to mean "All"
status: MalAnimeListStatusEnum.or(z.literal("")).optional(),
type: songTypesCodec.optional(),
}).strict();
type StatusParam = z.infer<typeof SearchSchema>["status"];
@@ -39,6 +41,7 @@ export const load: PageLoad = async ({ url, fetch, depends }) => {
const status = parsed.success
? normalizeStatus(parsed.data.status)
: undefined;
const types = parsed.success ? parsed.data.type : undefined;
const username = (mal ?? "").trim();
@@ -88,7 +91,7 @@ export const load: PageLoad = async ({ url, fetch, depends }) => {
await ensureSeeded({ fetch });
const malIds = malResponse.data.map((e) => e.node.id);
const songRows = await getSongsForMalAnimeIds(db, malIds);
const songRows = await getSongsForMalAnimeIds(db, malIds, types);
return {
LIST_QUERY_LIMIT,

View File

@@ -7,7 +7,7 @@ import {
const SEP = ",";
const songTypesCodec = z.codec(z.string(), z.array(AmqSongLinkType), {
export const songTypesCodec = z.codec(z.string(), z.array(AmqSongLinkType), {
decode: (str) =>
str
? decodeURIComponent(str)