← 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: [] });
  }
}