From 3b7d941d59104da7aa2c6c21b12664ff37414cde Mon Sep 17 00:00:00 2001 From: Josiah Glosson Date: Thu, 14 May 2026 08:33:36 -0500 Subject: [PATCH] Add `/api/tournament/:id/streams` route (#3067) --- .../routes/tournament.$id.streams.ts | 80 +++++++++++++++++++ app/features/api-public/schema.ts | 10 +++ app/routes.ts | 4 + 3 files changed, 94 insertions(+) create mode 100644 app/features/api-public/routes/tournament.$id.streams.ts diff --git a/app/features/api-public/routes/tournament.$id.streams.ts b/app/features/api-public/routes/tournament.$id.streams.ts new file mode 100644 index 000000000..9bc5bdd5b --- /dev/null +++ b/app/features/api-public/routes/tournament.$id.streams.ts @@ -0,0 +1,80 @@ +import { sql } from "kysely"; +import { jsonArrayFrom } from "kysely/helpers/sqlite"; +import type { LoaderFunctionArgs } from "react-router"; +import { z } from "zod"; +import { db } from "~/db/sql"; +import { notFoundIfFalsy, parseParams } from "~/utils/remix.server"; +import { id } from "~/utils/zod"; +import type { GetTournamentStreamsResponse } from "../schema"; + +const paramsSchema = z.object({ + id, +}); + +export const loader = async ({ params }: LoaderFunctionArgs) => { + const { id } = parseParams({ + params, + schema: paramsSchema, + }); + + const tournament = notFoundIfFalsy( + await db + .selectFrom("Tournament") + .select([ + jsonArrayFrom( + db + .selectFrom("LiveStream") + .leftJoin( + "TournamentTeamMember", + "TournamentTeamMember.userId", + "LiveStream.userId", + ) + .leftJoin( + "TournamentTeam", + "TournamentTeam.id", + "TournamentTeamMember.tournamentTeamId", + ) + .select([ + "LiveStream.userId", + "LiveStream.twitch", + "LiveStream.viewerCount", + ]) + .where("TournamentTeam.tournamentId", "=", id), + ).as("playerStreams"), + jsonArrayFrom( + db + .selectFrom("LiveStream") + .select(["LiveStream.twitch", "LiveStream.viewerCount"]) + .where( + sql`"LiveStream"."twitch" IN (SELECT value FROM json_each("Tournament"."castTwitchAccounts"))`, + ), + ).as("castStreams"), + ]) + .where("Tournament.id", "=", id) + .executeTakeFirst(), + ); + + const playerStreams: GetTournamentStreamsResponse = + tournament?.playerStreams.map((stream) => ({ + type: "PLAYER", + userId: stream.userId!, + platform: "TWITCH", + channelId: stream.twitch!, + viewerCount: stream.viewerCount!, + })) ?? []; + + const castStreams: GetTournamentStreamsResponse = + tournament?.castStreams.map((stream) => ({ + type: "CAST", + platform: "TWITCH", + channelId: stream.twitch!, + viewerCount: stream.viewerCount!, + })) ?? []; + + const result: GetTournamentStreamsResponse = [ + ...playerStreams, + ...castStreams, + ].sort((a, b) => b.viewerCount - a.viewerCount); + + return Response.json(result); +}; diff --git a/app/features/api-public/schema.ts b/app/features/api-public/schema.ts index 118a802ee..4679e4988 100644 --- a/app/features/api-public/schema.ts +++ b/app/features/api-public/schema.ts @@ -292,6 +292,16 @@ type TournamentCastChannel = { channelId: string; }; +/** GET /api/tournament/{tournamentId}/streams */ + +export type GetTournamentStreamsResponse = Array< + { + platform: "TWITCH"; + channelId: string; + viewerCount: number; + } & ({ type: "PLAYER"; userId: number } | { type: "CAST" }) +>; + /** GET /api/tournament-match/{matchId} */ export interface GetTournamentMatchResponse { diff --git a/app/routes.ts b/app/routes.ts index b5f98153e..bba7cc61b 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -339,6 +339,10 @@ export default [ "/tournament/:id/starting-brackets", "features/api-public/routes/tournament.$id.starting-brackets.ts", ), + route( + "/tournament/:id/streams", + "features/api-public/routes/tournament.$id.streams.ts", + ), route( "/tournament/:id/teams/:teamId/add-member", "features/api-public/routes/tournament.$id.teams.$teamId.add-member.ts",