songs page init
This commit is contained in:
@@ -1,4 +1,14 @@
|
|||||||
import { desc, eq, inArray, like } from "drizzle-orm";
|
import {
|
||||||
|
and,
|
||||||
|
between,
|
||||||
|
desc,
|
||||||
|
eq,
|
||||||
|
gte,
|
||||||
|
inArray,
|
||||||
|
like,
|
||||||
|
lte,
|
||||||
|
or,
|
||||||
|
} from "drizzle-orm";
|
||||||
import { anime, animeSongLinks, artists, groups, songs } from "$lib/db/schema";
|
import { anime, animeSongLinks, artists, groups, songs } from "$lib/db/schema";
|
||||||
import type { ClientDb } from "./index";
|
import type { ClientDb } from "./index";
|
||||||
|
|
||||||
@@ -183,6 +193,113 @@ export async function getSongsForMalAnimeIds(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define interfaces for filters
|
||||||
|
export interface SongFilters {
|
||||||
|
songName?: string;
|
||||||
|
artistName?: string;
|
||||||
|
animeName?: string; // Searches mainName, mainNameEn, mainNameJa
|
||||||
|
songTypes?: number[]; // 1: OP, 2: ED, 3: INS
|
||||||
|
globalPercentMin?: number; // 0-100
|
||||||
|
globalPercentMax?: number; // 0-100
|
||||||
|
category?: number; // 0: none, 1: instrumental, 2: chanting, 3: character, 4: standard
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSongsWithFilters(
|
||||||
|
db: ClientDb,
|
||||||
|
filters: SongFilters,
|
||||||
|
limit = DEFAULT_LIST_LIMIT,
|
||||||
|
) {
|
||||||
|
const safeLimit = clampLimit(limit);
|
||||||
|
const {
|
||||||
|
songName,
|
||||||
|
artistName,
|
||||||
|
animeName,
|
||||||
|
songTypes,
|
||||||
|
globalPercentMin,
|
||||||
|
globalPercentMax,
|
||||||
|
category,
|
||||||
|
} = filters;
|
||||||
|
|
||||||
|
const query = db
|
||||||
|
.select({
|
||||||
|
annSongId: songs.annSongId,
|
||||||
|
songName: songs.name,
|
||||||
|
fileName: songs.fileName,
|
||||||
|
globalPercent: songs.globalPercent,
|
||||||
|
category: songs.category,
|
||||||
|
|
||||||
|
type: animeSongLinks.type,
|
||||||
|
number: animeSongLinks.number,
|
||||||
|
|
||||||
|
animeAnnId: anime.annId,
|
||||||
|
animeMainName: anime.mainName,
|
||||||
|
animeMainNameEn: anime.mainNameEn,
|
||||||
|
animeMainNameJa: anime.mainNameJa,
|
||||||
|
|
||||||
|
artistName: artists.name,
|
||||||
|
groupName: groups.name,
|
||||||
|
})
|
||||||
|
.from(songs)
|
||||||
|
.leftJoin(artists, eq(artists.songArtistId, songs.songArtistId))
|
||||||
|
.leftJoin(groups, eq(groups.songGroupId, songs.songGroupId))
|
||||||
|
.innerJoin(animeSongLinks, eq(animeSongLinks.annSongId, songs.annSongId))
|
||||||
|
.innerJoin(anime, eq(anime.annId, animeSongLinks.annId))
|
||||||
|
.limit(safeLimit);
|
||||||
|
|
||||||
|
const conditions = [];
|
||||||
|
|
||||||
|
if (songName) {
|
||||||
|
conditions.push(like(songs.name, `%${songName}%`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artistName) {
|
||||||
|
// Search artistName OR groupName
|
||||||
|
const artistPattern = `%${artistName}%`;
|
||||||
|
conditions.push(
|
||||||
|
or(like(artists.name, artistPattern), like(groups.name, artistPattern)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animeName) {
|
||||||
|
// Search mainName, mainNameEn, or mainNameJa
|
||||||
|
const animePattern = `%${animeName}%`;
|
||||||
|
conditions.push(
|
||||||
|
or(
|
||||||
|
like(anime.mainName, animePattern),
|
||||||
|
like(anime.mainNameEn, animePattern),
|
||||||
|
like(anime.mainNameJa, animePattern),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songTypes && songTypes.length > 0) {
|
||||||
|
conditions.push(inArray(animeSongLinks.type, songTypes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalPercentMin !== undefined && globalPercentMax !== undefined) {
|
||||||
|
conditions.push(
|
||||||
|
between(songs.globalPercent, globalPercentMin, globalPercentMax),
|
||||||
|
);
|
||||||
|
} else if (globalPercentMin !== undefined) {
|
||||||
|
conditions.push(gte(songs.globalPercent, globalPercentMin));
|
||||||
|
} else if (globalPercentMax !== undefined) {
|
||||||
|
conditions.push(lte(songs.globalPercent, globalPercentMax));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category !== undefined) {
|
||||||
|
conditions.push(eq(songs.category, category));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.length > 0) {
|
||||||
|
query.where(and(...conditions));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order by song name for now, can add more sophisticated sorting later
|
||||||
|
query.orderBy(songs.name);
|
||||||
|
|
||||||
|
return query.execute();
|
||||||
|
}
|
||||||
|
|
||||||
function clampLimit(limit: number) {
|
function clampLimit(limit: number) {
|
||||||
const n = Number(limit);
|
const n = Number(limit);
|
||||||
if (!Number.isFinite(n)) return DEFAULT_LIST_LIMIT;
|
if (!Number.isFinite(n)) return DEFAULT_LIST_LIMIT;
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from "./season";
|
export * from "./season";
|
||||||
|
export * from "./song-utils";
|
||||||
|
|||||||
20
src/lib/utils/amq/song-utils.ts
Normal file
20
src/lib/utils/amq/song-utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export const SongTypeMap: Record<string, number> = {
|
||||||
|
OP: 1,
|
||||||
|
ED: 2,
|
||||||
|
INS: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map number back to string for displaying in UI
|
||||||
|
export const SongTypeReverseMap: Record<number, string> = {
|
||||||
|
1: "OP",
|
||||||
|
2: "ED",
|
||||||
|
3: "INS",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SongCategoryMap: Record<number, string> = {
|
||||||
|
0: "None",
|
||||||
|
1: "Instrumental",
|
||||||
|
2: "Chanting",
|
||||||
|
3: "Character",
|
||||||
|
4: "Standard",
|
||||||
|
};
|
||||||
303
src/routes/songs/+page.svelte
Normal file
303
src/routes/songs/+page.svelte
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { useSearchParams } from "runed/kit";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import { invalidate } from "$app/navigation";
|
||||||
|
import SongEntry from "$lib/components/SongEntry.svelte";
|
||||||
|
import { db as clientDb } from "$lib/db/client-db";
|
||||||
|
import { addAllToQueue, playAllNext } from "$lib/player/player.svelte";
|
||||||
|
import { trackFromSongRow } from "$lib/player/types";
|
||||||
|
import { SongCategoryMap, SongTypeReverseMap } from "$lib/utils/amq";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
|
// Zod schema for URL search parameters to ensure type safety and parsing
|
||||||
|
const SongsSearchSchema = z
|
||||||
|
.object({
|
||||||
|
q: z.string().default(""), // song name
|
||||||
|
artist: z.string().default(""), // artist name
|
||||||
|
anime: z.string().default(""), // anime mainName
|
||||||
|
type: z.array(z.string()).default([]), // song type (e.g., "OP", "ED", "INS")
|
||||||
|
gpm: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((s) => (s ? parseInt(s, 10) : undefined)), // global percent min
|
||||||
|
gpx: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((s) => (s ? parseInt(s, 10) : undefined)), // global percent max
|
||||||
|
cat: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((s) => (s ? parseInt(s, 10) : undefined)), // category
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const params = useSearchParams(SongsSearchSchema, {
|
||||||
|
pushHistory: false,
|
||||||
|
showDefaults: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
// Local form values, bound to inputs, which will update params.
|
||||||
|
let form = $state({
|
||||||
|
q: params.q,
|
||||||
|
artist: params.artist,
|
||||||
|
anime: params.anime,
|
||||||
|
type: params.type,
|
||||||
|
gpm: params.gpm ?? "",
|
||||||
|
gpx: params.gpx ?? "",
|
||||||
|
cat: params.cat ?? "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// If SSR returned no songRows (because client DB wasn't available),
|
||||||
|
// re-run load on the client once the DB is ready by invalidating.
|
||||||
|
onMount(() => {
|
||||||
|
if (data.songRows.length > 0) return;
|
||||||
|
|
||||||
|
if (clientDb) {
|
||||||
|
void invalidate("clientdb:songs");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function songArtistLabel(r: (typeof data.songRows)[number]) {
|
||||||
|
// Use animeMainName as the primary anime name for display
|
||||||
|
return r.artistName ?? r.groupName ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to map song type number back to string for UI display
|
||||||
|
function getSongTypeLabel(type: number): string {
|
||||||
|
return SongTypeReverseMap[type] || `Type ${type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to map song category number back to string for UI display
|
||||||
|
function getSongCategoryLabel(category: number): string {
|
||||||
|
return SongCategoryMap[category] || `Category ${category}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracksFromResults = $derived.by(() =>
|
||||||
|
data.songRows
|
||||||
|
.map((r) =>
|
||||||
|
trackFromSongRow({
|
||||||
|
annSongId: r.annSongId,
|
||||||
|
animeName: r.animeMainName, // Use animeMainName from the query result
|
||||||
|
type: r.type,
|
||||||
|
number: r.number,
|
||||||
|
songName: r.songName,
|
||||||
|
artistName: songArtistLabel(r),
|
||||||
|
fileName: r.fileName,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.filter((t) => t !== null),
|
||||||
|
);
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
params.q = form.q;
|
||||||
|
params.artist = form.artist;
|
||||||
|
params.anime = form.anime;
|
||||||
|
params.type = form.type;
|
||||||
|
// Only set gpm/gpx if they are valid numbers
|
||||||
|
params.gpm = form.gpm !== "" ? String(form.gpm) : undefined;
|
||||||
|
params.gpx = form.gpx !== "" ? String(form.gpx) : undefined;
|
||||||
|
// Only set cat if it's a valid number
|
||||||
|
params.cat = form.cat !== "" ? String(form.cat) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle checkbox changes for song types
|
||||||
|
function handleSongTypeChange(typeValue: string, isChecked: boolean) {
|
||||||
|
if (isChecked) {
|
||||||
|
form.type = [...form.type, typeValue];
|
||||||
|
} else {
|
||||||
|
form.type = form.type.filter((t) => t !== typeValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-2xl font-semibold">Songs Search</h1>
|
||||||
|
|
||||||
|
<p class="mt-2 text-sm text-muted-foreground">
|
||||||
|
{#if !clientDb}
|
||||||
|
Loading DB...
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="mt-4 flex flex-col gap-4"
|
||||||
|
onsubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
applyFilters();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-muted-foreground" for="song-name"
|
||||||
|
>Song Name</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="song-name"
|
||||||
|
class="rounded border px-3 py-2 text-sm"
|
||||||
|
placeholder="e.g. Renai Circulation"
|
||||||
|
bind:value={form.q}
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-muted-foreground" for="artist-name"
|
||||||
|
>Artist Name</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="artist-name"
|
||||||
|
class="rounded border px-3 py-2 text-sm"
|
||||||
|
placeholder="e.g. Kana Hanazawa"
|
||||||
|
bind:value={form.artist}
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-muted-foreground" for="anime-name"
|
||||||
|
>Anime Name</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="anime-name"
|
||||||
|
class="rounded border px-3 py-2 text-sm"
|
||||||
|
placeholder="e.g. Bakemonogatari"
|
||||||
|
bind:value={form.anime}
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-muted-foreground">Song Type</label>
|
||||||
|
<div class="flex flex-wrap gap-x-4 gap-y-2">
|
||||||
|
{#each [1, 2, 3] as typeNum}
|
||||||
|
<label class="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={getSongTypeLabel(typeNum)}
|
||||||
|
checked={form.type.includes(getSongTypeLabel(typeNum))}
|
||||||
|
onchange={(e) =>
|
||||||
|
handleSongTypeChange(e.target.value, e.target.checked)}
|
||||||
|
/>
|
||||||
|
{getSongTypeLabel(typeNum)}
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-muted-foreground" for="global-percent-min"
|
||||||
|
>Global Percent Range</label
|
||||||
|
>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
id="global-percent-min"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
class="rounded border px-3 py-2 text-sm w-1/2"
|
||||||
|
placeholder="Min (0-100)"
|
||||||
|
bind:value={form.gpm}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
id="global-percent-max"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
class="rounded border px-3 py-2 text-sm w-1/2"
|
||||||
|
placeholder="Max (0-100)"
|
||||||
|
bind:value={form.gpx}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm text-muted-foreground" for="song-category"
|
||||||
|
>Category</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="song-category"
|
||||||
|
class="rounded border px-3 py-2 text-sm"
|
||||||
|
bind:value={form.cat}
|
||||||
|
>
|
||||||
|
<option value="">All</option>
|
||||||
|
{#each Object.entries(SongCategoryMap) as [key, value]}
|
||||||
|
<option value={key}>{value}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded border px-4 py-2 text-sm bg-blue-500 text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Search Songs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#if data.songRows.length > 0}
|
||||||
|
<div class="mt-6 flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded border px-3 py-2 text-sm"
|
||||||
|
onclick={() => addAllToQueue(tracksFromResults)}
|
||||||
|
disabled={tracksFromResults.length === 0}
|
||||||
|
>
|
||||||
|
Add all to queue
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded border px-3 py-2 text-sm"
|
||||||
|
onclick={() => playAllNext(tracksFromResults)}
|
||||||
|
disabled={tracksFromResults.length === 0}
|
||||||
|
>
|
||||||
|
Play all next
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if tracksFromResults.length !== data.songRows.length}
|
||||||
|
<span class="self-center text-sm text-muted-foreground">
|
||||||
|
({tracksFromResults.length} playable)
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mt-6 text-lg font-semibold">Songs</h2>
|
||||||
|
|
||||||
|
<ul class="mt-3 space-y-2">
|
||||||
|
{#each data.songRows as r (String(r.annSongId) + "-" + String(r.animeAnnId) + "-" + String(r.type) + "-" + String(r.number))}
|
||||||
|
<li>
|
||||||
|
<SongEntry
|
||||||
|
annSongId={r.annSongId}
|
||||||
|
animeName={r.animeMainName}
|
||||||
|
type={r.type}
|
||||||
|
number={r.number}
|
||||||
|
songName={r.songName}
|
||||||
|
artistName={songArtistLabel(r)}
|
||||||
|
fileName={r.fileName}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else if Object.values(form).some((val) => (Array.isArray(val) && val.length > 0) || (typeof val === "string" && val.trim() !== "") || typeof val === "number")}
|
||||||
|
<p class="mt-4 text-sm text-muted-foreground">
|
||||||
|
No songs found matching your criteria.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="mt-4 text-sm text-muted-foreground">
|
||||||
|
Use the filters above to search for songs.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !browser}
|
||||||
|
Loading stuff...
|
||||||
|
{/if}
|
||||||
78
src/routes/songs/+page.ts
Normal file
78
src/routes/songs/+page.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import type { SongFilters } from "$lib/db/client-db";
|
||||||
|
import { db, ensureSeeded, getSongsWithFilters } from "$lib/db/client-db";
|
||||||
|
import {
|
||||||
|
SongCategoryMap,
|
||||||
|
SongTypeMap,
|
||||||
|
SongTypeReverseMap,
|
||||||
|
} from "$lib/utils/amq";
|
||||||
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
|
const SearchSchema = z
|
||||||
|
.object({
|
||||||
|
q: z.string().optional(), // song name
|
||||||
|
artist: z.string().optional(), // artist name
|
||||||
|
anime: z.string().optional(), // anime mainName
|
||||||
|
type: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((s) => {
|
||||||
|
if (!s) return undefined;
|
||||||
|
return s
|
||||||
|
.split(",")
|
||||||
|
.map((t) => SongTypeMap[t.trim().toUpperCase()])
|
||||||
|
.filter((n) => n !== undefined);
|
||||||
|
}),
|
||||||
|
gpm: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((s) => (s ? parseInt(s, 10) : undefined)), // global percent min
|
||||||
|
gpx: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((s) => (s ? parseInt(s, 10) : undefined)), // global percent max
|
||||||
|
cat: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((s) => (s ? parseInt(s, 10) : undefined)), // category
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ url, fetch, depends }) => {
|
||||||
|
depends("clientdb:songs");
|
||||||
|
|
||||||
|
const parsed = SearchSchema.safeParse(
|
||||||
|
Object.fromEntries(url.searchParams.entries()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const filters: SongFilters = {};
|
||||||
|
if (parsed.success) {
|
||||||
|
if (parsed.data.q) filters.songName = parsed.data.q;
|
||||||
|
if (parsed.data.artist) filters.artistName = parsed.data.artist;
|
||||||
|
if (parsed.data.anime) filters.animeName = parsed.data.anime;
|
||||||
|
if (parsed.data.type && parsed.data.type.length > 0)
|
||||||
|
filters.songTypes = parsed.data.type;
|
||||||
|
if (parsed.data.gpm !== undefined)
|
||||||
|
filters.globalPercentMin = parsed.data.gpm;
|
||||||
|
if (parsed.data.gpx !== undefined)
|
||||||
|
filters.globalPercentMax = parsed.data.gpx;
|
||||||
|
if (parsed.data.cat !== undefined) filters.category = parsed.data.cat;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client-only DB: on the server `db` is null, so return [] and let hydration re-run load in browser.
|
||||||
|
if (!db) {
|
||||||
|
return {
|
||||||
|
filters: parsed.success ? parsed.data : {}, // Return original parsed data for form state
|
||||||
|
songRows: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureSeeded({ fetch });
|
||||||
|
|
||||||
|
const songRows = await getSongsWithFilters(db, filters);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters: parsed.success ? parsed.data : {}, // Return original parsed data for form state
|
||||||
|
songRows,
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user