diff --git a/src/lib/db/client-db/queries.ts b/src/lib/db/client-db/queries.ts index b4d3dc9..6fc096a 100644 --- a/src/lib/db/client-db/queries.ts +++ b/src/lib/db/client-db/queries.ts @@ -1,5 +1,11 @@ -import { desc, like } from "drizzle-orm"; -import { animeTable } from "$lib/db/schema"; +import { desc, eq, like } from "drizzle-orm"; +import { + animeSongLinksTable, + animeTable, + artistsTable, + groupsTable, + songsTable, +} from "$lib/db/schema"; import type { ClientDb } from "./index"; /** @@ -75,6 +81,69 @@ export async function searchAnimeByName( .limit(safeLimit); } +/** + * Fetch a single anime plus its linked songs, including: + * - anime name + * - link metadata: type + number (OP1/ED2/etc) + * - song metadata: song name, artist/group name, fileName for audio playback + * + * Note: this assumes the SQLite snapshot has `songs.file_name`, `songs.song_artist_id`, + * and `songs.song_group_id` populated. + */ +export async function getAnimeWithSongsByAnnId(db: ClientDb, annId: number) { + const animeRows = await db + .select({ + annId: animeTable.annId, + mainName: animeTable.mainName, + year: animeTable.year, + seasonId: animeTable.seasonId, + malId: animeTable.malId, + }) + .from(animeTable) + .where(eq(animeTable.annId, annId)) + .limit(1); + + const anime = animeRows[0]; + if (!anime) return null; + + const rows = await db + .select({ + annSongId: animeSongLinksTable.annSongId, + type: animeSongLinksTable.type, + number: animeSongLinksTable.number, + + songName: songsTable.name, + fileName: songsTable.fileName, + + artistName: artistsTable.name, + groupName: groupsTable.name, + }) + .from(animeSongLinksTable) + .innerJoin( + songsTable, + eq(songsTable.annSongId, animeSongLinksTable.annSongId), + ) + .leftJoin( + artistsTable, + eq(artistsTable.songArtistId, songsTable.songArtistId), + ) + .leftJoin(groupsTable, eq(groupsTable.songGroupId, songsTable.songGroupId)) + .where(eq(animeSongLinksTable.annId, annId)) + .orderBy(desc(animeSongLinksTable.type), desc(animeSongLinksTable.number)); + + return { + anime, + songs: rows.map((r) => ({ + annSongId: r.annSongId, + type: r.type, + number: r.number, + songName: r.songName, + fileName: r.fileName, + artistName: r.artistName ?? r.groupName ?? null, + })), + }; +} + function clampLimit(limit: number) { const n = Number(limit); if (!Number.isFinite(n)) return DEFAULT_LIST_LIMIT; diff --git a/src/lib/db/import-amq.ts b/src/lib/db/import-amq.ts index e95263f..9592a0d 100644 --- a/src/lib/db/import-amq.ts +++ b/src/lib/db/import-amq.ts @@ -331,6 +331,13 @@ export async function importAmqData( songId: s.songId, name: s.name, category: s.category, + fileName: s.fileName, + songArtistId: s.songArtistId, + songGroupId: s.songGroupId, + composerArtistId: s.composerArtistId, + composerGroupId: s.composerGroupId, + arrangerArtistId: s.arrangerArtistId, + arrangerGroupId: s.arrangerGroupId, })); for (const batch of chunk(songRows, batchSize)) { @@ -343,6 +350,13 @@ export async function importAmqData( songId: songsTable.songId, name: songsTable.name, category: songsTable.category, + fileName: songsTable.fileName, + songArtistId: songsTable.songArtistId, + songGroupId: songsTable.songGroupId, + composerArtistId: songsTable.composerArtistId, + composerGroupId: songsTable.composerGroupId, + arrangerArtistId: songsTable.arrangerArtistId, + arrangerGroupId: songsTable.arrangerGroupId, }, }) .run(); diff --git a/src/lib/db/schema/index.ts b/src/lib/db/schema/index.ts index f4cb47f..2b90535 100644 --- a/src/lib/db/schema/index.ts +++ b/src/lib/db/schema/index.ts @@ -81,9 +81,51 @@ export const songsTable = sqliteTable( * none(0), instrumental(1), chanting(2), character(3), standard(4) */ category: integer("category").notNull(), + + /** + * Song audio filename used by the AMQ CDN: + * https://nawdist.animemusicquiz.com/{fileName} + */ + fileName: text("file_name"), + + /** + * Primary artist/group ids for this song (nullable in source). + * These reference existing `artists` / `groups` rows. + */ + songArtistId: integer("song_artist_id").references( + () => artistsTable.songArtistId, + { onDelete: "set null" }, + ), + songGroupId: integer("song_group_id").references( + () => groupsTable.songGroupId, + { onDelete: "set null" }, + ), + + /** + * Additional contributor ids (nullable in source) + */ + composerArtistId: integer("composer_artist_id").references( + () => artistsTable.songArtistId, + { onDelete: "set null" }, + ), + composerGroupId: integer("composer_group_id").references( + () => groupsTable.songGroupId, + { onDelete: "set null" }, + ), + arrangerArtistId: integer("arranger_artist_id").references( + () => artistsTable.songArtistId, + { onDelete: "set null" }, + ), + arrangerGroupId: integer("arranger_group_id").references( + () => groupsTable.songGroupId, + { onDelete: "set null" }, + ), }, (t) => ({ songIdIndex: index("songs_song_id_idx").on(t.songId), + fileNameIndex: index("songs_file_name_idx").on(t.fileName), + songArtistIdIndex: index("songs_song_artist_id_idx").on(t.songArtistId), + songGroupIdIndex: index("songs_song_group_id_idx").on(t.songGroupId), }), ); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9666f3a..eacf976 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -44,6 +44,10 @@ } } + function animeHref(annId: number) { + return `/anime/${annId}`; + } + async function loadInitial() { status = "loading"; error = null; @@ -112,7 +116,9 @@
Loading anime…
+{:else if status === "error"} +Error: {error}
+{:else if status === "not-found"} ++ The requested anime entry doesn’t exist (or the route param wasn’t a valid + ANN id). +
+{:else if status === "ready" && data} ++ {data.anime.year}{seasonName(data.anime.seasonId)} • ANN {data.anime + .annId} • MAL {data.anime.malId} +
++ No linked songs found for this anime. +
+ {:else} ++ No audio filename in the snapshot for this song. +
+ {/if} +