import { fetch } from "https://esm.town/v/std/fetch";
export const YoutubeTranscript = async (url: string, metadata = true) => {
const RE_YOUTUBE =
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i;
const YouTubeKey = {
apiKey: "INNERTUBE_API_KEY",
serializedShareEntity: "serializedShareEntity",
visitorData: "VISITOR_DATA",
sessionId: "sessionId",
clickTrackingParams: "clickTrackingParams",
} as const;
type ObjectValues<T> = T[keyof T];
type YouTubeKeyRaw = ObjectValues<typeof YouTubeKey>;
class YoutubeTranscriptError extends Error {
constructor(message: string) {
super(`[YoutubeTranscript] 🚨 ${message}`);
}
}
interface TranscriptConfig {
lang?: string;
country?: string;
}
interface TranscriptSegment {
text: string;
duration: number;
offset: number;
}
interface TranscriptData {
responseContext: {
serviceTrackingParams: {
service: string;
params: {
key: string;
value: string;
}[];
}[];
mainAppWebResponseContext: {
loggedOut: boolean;
trackingParam: string;
};
webResponseContextExtensionData: {
hasDecorated: boolean;
};
};
actions: {
clickTrackingParams: string;
updateEngagementPanelAction: {
targetId: string;
content: {
transcriptRenderer: {
body: {
transcriptBodyRenderer: {
cueGroups: {
transcriptCueGroupRenderer: {
formattedStartOffset: {
simpleText: string;
};
cues: {
transcriptCueRenderer: {
cue: {
simpleText: string;
};
startOffsetMs: string;
durationMs: string;
};
}[];
};
}[];
};
};
};
};
};
}[];
}
class YoutubeTranscript {
static async fetchTranscript(
videoId: string,
config?: TranscriptConfig,
): Promise<TranscriptSegment[]> {
const identifier = this.extractVideoId(videoId);
try {
const textResponse = await fetch(
`https://www.youtube.com/watch?v=${identifier}`,
);
const body = await textResponse.text();
const apiKey = this.extractValue(body, YouTubeKey.apiKey);
if (!apiKey || !apiKey.length) {
throw new YoutubeTranscriptError(
`Failed to extract ${YouTubeKey.apiKey}`,
);