Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/utils/twitch-server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { getVodWithMarkers } from "./twitch-server";

// The creator has signed in before, so Clerk has their Twitch token
vi.mock("@clerk/nextjs/server", () => ({
clerkClient: async () => ({
users: {
getUserList: async () => ({ data: [{ id: "creator" }] }),
getUserOauthAccessToken: async () => ({
data: [{ token: "creator-token" }],
}),
},
}),
}));

const vod = {
created_at: "2026-09-24T00:00:00Z",
duration: "1h0m0s",
user_login: "creator",
};

// Stubs the videos request, then the markers request
const stubTwitch = (markersResponse: Response) =>
vi.stubGlobal(
"fetch",
vi
.fn()
.mockResolvedValueOnce(Response.json({ data: [vod] }))
.mockResolvedValueOnce(markersResponse)
);

afterEach(() => {
vi.unstubAllGlobals();
});

describe("getVodWithMarkers", () => {
it("throws when the markers request fails", async () => {
stubTwitch(
Response.json({ error: "Unauthorized", status: 401 }, { status: 401 })
);

await expect(getVodWithMarkers("123", "viewer-token")).rejects.toThrow(
"Twitch markers request failed: 401"
);
});

it("gives no markers when Twitch sends an empty videos list", async () => {
stubTwitch(Response.json({ data: [{ videos: [] }] }));

await expect(getVodWithMarkers("123", "viewer-token")).resolves.toEqual({
...vod,
markers: [],
});
});
});
36 changes: 19 additions & 17 deletions src/utils/twitch-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export type VOD = {
duration: string;
};

// Used for vod markers
// Used for VOD markers and the live topic embed
const getValidTokenForCreator = async (creatorName: string) => {
// Get token for the input displayName IF THEY HAVE SIGNED IN BEFORE
const clerk = await clerkClient();
Expand All @@ -49,8 +49,6 @@ const getValidTokenForCreator = async (creatorName: string) => {

const creatorFoundInClerk = response.data[0];

console.log("found in clerk?", creatorFoundInClerk);

// Early escape if we don't find this user in Clerk
if (!creatorFoundInClerk) {
throw new Error("User not found in Clerk");
Expand All @@ -59,6 +57,9 @@ const getValidTokenForCreator = async (creatorName: string) => {
return await getTwitchTokenFromClerk(creatorFoundInClerk.id);
};

// Loads a VOD and its markers. Markers are read with the creator's stored
// token, so any signed-in user can view a connected creator's markers.
// Throws on Twitch errors, so a failed request is not read as "no markers".
export const getVodWithMarkers = async (vodId: string, token: string) => {
const vodResponse = await fetch(
`https://api.twitch.tv/helix/videos?id=${vodId}`,
Expand All @@ -69,16 +70,18 @@ export const getVodWithMarkers = async (vodId: string, token: string) => {
cache: "no-store",
}
);
console.log("VOD RESPONSE", vodResponse.status);

const vodData = await vodResponse.json();
console.log("VOD DATA", vodData);
if (!vodResponse.ok) {
throw new Error(`Twitch videos request failed: ${vodResponse.status}`);
}

const creatorName = vodData?.data?.[0]?.user_login;
const vodData = (await vodResponse.json()) as {
data?: (Omit<VOD, "markers"> & { user_login: string })[];
};
const vod = vodData.data?.[0];

if (!creatorName) throw new Error("could not find vod data or user login");
if (!vod) throw new Error("could not find vod data or user login");

const tokenForMarkers = await getValidTokenForCreator(creatorName);
const tokenForMarkers = await getValidTokenForCreator(vod.user_login);

const markersResponse = await fetch(
`https://api.twitch.tv/helix/streams/markers?video_id=${vodId}&first=100`,
Expand All @@ -88,15 +91,14 @@ export const getVodWithMarkers = async (vodId: string, token: string) => {
next: { revalidate: 60 },
}
);
if (!markersResponse.ok) {
throw new Error(`Twitch markers request failed: ${markersResponse.status}`);
}

console.log("MARKER RESPONSE", markersResponse.status);

const markersData = await markersResponse.json();
console.log("MARKER DATA", markersData);

const markers = markersData?.data?.[0]?.videos?.[0]["markers"] ?? [];
const markersData = (await markersResponse.json()) as TwitchMarkersResponse;
const markers = markersData.data?.[0]?.videos?.[0]?.markers ?? [];

return { ...vodData?.data?.[0], markers } as VOD;
return { ...vod, markers };
};

export const getTwitchTokenFromClerk = async (clerkUserId: string) => {
Expand Down
Loading