← back to Nineoh Guide
apps/web/app/api/episodes/route.ts
78 lines
import { NextResponse } from "next/server";
import { pool } from "@/lib/db";
import { EpisodeSchema } from "@nineoh/core";
export const dynamic = "force-dynamic";
/**
* Shared API surface consumed by BOTH the web app and the Expo app.
* Rows are mapped to camelCase and validated against the shared EpisodeSchema
* before they leave the server, so a malformed row can never reach a client.
*/
export async function GET() {
try {
const { rows } = await pool.query(
`select id, season_number, episode_number, title, air_date,
writer, director, summary_short_original, summary_full_original,
spoiler_rating, external_ids
from episodes
order by season_number, episode_number
limit 500`
);
// Series regulars (attach to every episode — they're the show's stars).
const { rows: regRows } = await pool.query(
`select cp.name, ch.name as character
from credits cr
join cast_people cp on cp.id = cr.person_id
left join characters ch on ch.id = cr.character_id
where cr.credit_type = 'main-cast'
order by coalesce(cr.billing_order, 999), cp.name`
);
const regulars = regRows.map((r) => ({
name: r.name as string,
character: (r.character ?? null) as string | null,
}));
// This episode's guest stars (factual, per-episode, from TVmaze).
const { rows: guestRows } = await pool.query(
`select episode_id, person_name, character_name
from episode_guest_cast
order by episode_id, ord`
);
const guestsByEp = new Map<string, { name: string; character: string | null }[]>();
for (const g of guestRows) {
const arr = guestsByEp.get(g.episode_id) ?? [];
arr.push({ name: g.person_name, character: g.character_name ?? null });
guestsByEp.set(g.episode_id, arr);
}
const episodes = rows
.map((r) => {
const seen = new Set<string>();
const cast = [...regulars, ...(guestsByEp.get(r.id) ?? [])].filter((c) =>
seen.has(c.name) ? false : (seen.add(c.name), true)
);
return EpisodeSchema.safeParse({
id: r.id,
seasonNumber: r.season_number,
episodeNumber: r.episode_number,
title: r.title,
airDate: r.air_date ? new Date(r.air_date).toISOString().slice(0, 10) : null,
writer: r.writer,
director: r.director,
summaryShortOriginal: r.summary_short_original,
summaryFullOriginal: r.summary_full_original,
spoilerRating: r.spoiler_rating ?? 0,
externalIds: r.external_ids ?? {},
cast,
});
})
.filter((p) => p.success)
.map((p) => p.data);
return NextResponse.json({ episodes });
} catch {
return NextResponse.json({ episodes: [] });
}
}