Files
camila/node_modules/youtube-transcript/dist/commonjs/index.js
T

269 lines
11 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.YoutubeTranscript = exports.YoutubeTranscriptNotAvailableLanguageError = exports.YoutubeTranscriptNotAvailableError = exports.YoutubeTranscriptDisabledError = exports.YoutubeTranscriptVideoUnavailableError = exports.YoutubeTranscriptTooManyRequestError = exports.YoutubeTranscriptError = void 0;
exports.fetchTranscript = fetchTranscript;
const RE_YOUTUBE = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i;
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36,gzip(gfe)';
const RE_XML_TRANSCRIPT = /<text start="([^"]*)" dur="([^"]*)">([^<]*)<\/text>/g;
const INNERTUBE_API_URL = 'https://www.youtube.com/youtubei/v1/player?prettyPrint=false';
const INNERTUBE_CLIENT_VERSION = '20.10.38';
const INNERTUBE_CONTEXT = {
client: {
clientName: 'ANDROID',
clientVersion: INNERTUBE_CLIENT_VERSION,
},
};
const INNERTUBE_USER_AGENT = `com.google.android.youtube/${INNERTUBE_CLIENT_VERSION} (Linux; U; Android 14)`;
class YoutubeTranscriptError extends Error {
constructor(message) {
super(`[YoutubeTranscript] 🚨 ${message}`);
}
}
exports.YoutubeTranscriptError = YoutubeTranscriptError;
class YoutubeTranscriptTooManyRequestError extends YoutubeTranscriptError {
constructor() {
super('YouTube is receiving too many requests from this IP and now requires solving a captcha to continue');
}
}
exports.YoutubeTranscriptTooManyRequestError = YoutubeTranscriptTooManyRequestError;
class YoutubeTranscriptVideoUnavailableError extends YoutubeTranscriptError {
constructor(videoId) {
super(`The video is no longer available (${videoId})`);
}
}
exports.YoutubeTranscriptVideoUnavailableError = YoutubeTranscriptVideoUnavailableError;
class YoutubeTranscriptDisabledError extends YoutubeTranscriptError {
constructor(videoId) {
super(`Transcript is disabled on this video (${videoId})`);
}
}
exports.YoutubeTranscriptDisabledError = YoutubeTranscriptDisabledError;
class YoutubeTranscriptNotAvailableError extends YoutubeTranscriptError {
constructor(videoId) {
super(`No transcripts are available for this video (${videoId})`);
}
}
exports.YoutubeTranscriptNotAvailableError = YoutubeTranscriptNotAvailableError;
class YoutubeTranscriptNotAvailableLanguageError extends YoutubeTranscriptError {
constructor(lang, availableLangs, videoId) {
super(`No transcripts are available in ${lang} this video (${videoId}). Available languages: ${availableLangs.join(', ')}`);
}
}
exports.YoutubeTranscriptNotAvailableLanguageError = YoutubeTranscriptNotAvailableLanguageError;
/**
* Class to retrieve transcript if exist
*/
class YoutubeTranscript {
/**
* Fetch transcript from YTB Video
* @param videoId Video url or video identifier
* @param config Get transcript in a specific language ISO
*/
static async fetchTranscript(videoId, config) {
const identifier = this.retrieveVideoId(videoId);
// Try InnerTube API first
const innerTubeResult = await this.fetchViaInnerTube(identifier, config);
if (innerTubeResult) {
return innerTubeResult;
}
// Fall back to HTML scraping
return this.fetchViaWebPage(identifier, videoId, config);
}
/**
* Fetch transcript via the InnerTube API (Android client context)
*/
static async fetchViaInnerTube(identifier, config) {
try {
const fetchFn = config?.fetch ?? fetch;
const resp = await fetchFn(INNERTUBE_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': INNERTUBE_USER_AGENT,
},
body: JSON.stringify({
context: INNERTUBE_CONTEXT,
videoId: identifier,
}),
});
if (!resp.ok)
return undefined;
const data = await resp.json();
const captionTracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks;
if (!Array.isArray(captionTracks) || captionTracks.length === 0) {
return undefined;
}
return this.fetchTranscriptFromTracks(captionTracks, identifier, config);
}
catch {
return undefined;
}
}
/**
* Fetch transcript via web page HTML scraping (fallback)
*/
static async fetchViaWebPage(identifier, originalVideoId, config) {
const fetchFn = config?.fetch ?? fetch;
const videoPageResponse = await fetchFn(`https://www.youtube.com/watch?v=${identifier}`, {
headers: {
...(config?.lang && { 'Accept-Language': config.lang }),
'User-Agent': USER_AGENT,
},
});
const videoPageBody = await videoPageResponse.text();
if (videoPageBody.includes('class="g-recaptcha"')) {
throw new YoutubeTranscriptTooManyRequestError();
}
if (!videoPageBody.includes('"playabilityStatus":')) {
throw new YoutubeTranscriptVideoUnavailableError(originalVideoId);
}
const playerResponse = this.parseInlineJson(videoPageBody, 'ytInitialPlayerResponse');
const captionTracks = playerResponse?.captions?.playerCaptionsTracklistRenderer?.captionTracks;
if (!Array.isArray(captionTracks) || captionTracks.length === 0) {
throw new YoutubeTranscriptDisabledError(originalVideoId);
}
return this.fetchTranscriptFromTracks(captionTracks, originalVideoId, config);
}
/**
* Extract a JSON object assigned to a global variable in inline script tags
*/
static parseInlineJson(html, globalName) {
const startToken = `var ${globalName} = `;
const startIndex = html.indexOf(startToken);
if (startIndex === -1)
return null;
const jsonStart = startIndex + startToken.length;
let depth = 0;
for (let i = jsonStart; i < html.length; i++) {
if (html[i] === '{')
depth++;
else if (html[i] === '}') {
depth--;
if (depth === 0) {
try {
return JSON.parse(html.slice(jsonStart, i + 1));
}
catch {
return null;
}
}
}
}
return null;
}
/**
* Given caption tracks, select the right one, fetch and parse the transcript XML
*/
static async fetchTranscriptFromTracks(captionTracks, videoId, config) {
if (config?.lang &&
!captionTracks.some((track) => track.languageCode === config?.lang)) {
throw new YoutubeTranscriptNotAvailableLanguageError(config?.lang, captionTracks.map((track) => track.languageCode), videoId);
}
const track = config?.lang
? captionTracks.find((track) => track.languageCode === config?.lang)
: captionTracks[0];
const transcriptURL = track.baseUrl;
// Validate URL to prevent SSRF
try {
const captionUrl = new URL(transcriptURL);
if (!captionUrl.hostname.endsWith('.youtube.com')) {
throw new YoutubeTranscriptNotAvailableError(videoId);
}
}
catch (e) {
if (e instanceof YoutubeTranscriptError)
throw e;
throw new YoutubeTranscriptNotAvailableError(videoId);
}
const fetchFn = config?.fetch ?? fetch;
const transcriptResponse = await fetchFn(transcriptURL, {
headers: {
...(config?.lang && { 'Accept-Language': config.lang }),
'User-Agent': USER_AGENT,
},
});
if (!transcriptResponse.ok) {
throw new YoutubeTranscriptNotAvailableError(videoId);
}
const transcriptBody = await transcriptResponse.text();
const lang = config?.lang ?? captionTracks[0].languageCode;
return this.parseTranscriptXml(transcriptBody, lang);
}
/**
* Parse transcript XML, supporting both srv3 format (<p t="ms">) and
* classic format (<text start="s" dur="s">)
*/
static parseTranscriptXml(xml, lang) {
const results = [];
// Try srv3 format first: <p t="ms" d="ms"><s>word</s>...</p>
const pRegex = /<p\s+t="(\d+)"\s+d="(\d+)"[^>]*>([\s\S]*?)<\/p>/g;
let match;
while ((match = pRegex.exec(xml)) !== null) {
const startMs = parseInt(match[1], 10);
const durMs = parseInt(match[2], 10);
const inner = match[3];
let text = '';
const sRegex = /<s[^>]*>([^<]*)<\/s>/g;
let sMatch;
while ((sMatch = sRegex.exec(inner)) !== null) {
text += sMatch[1];
}
if (!text) {
text = inner.replace(/<[^>]+>/g, '');
}
text = this.decodeEntities(text).trim();
if (text) {
results.push({
text,
duration: durMs,
offset: startMs,
lang,
});
}
}
if (results.length > 0)
return results;
// Fall back to classic format: <text start="s" dur="s">content</text>
const classicResults = [...xml.matchAll(RE_XML_TRANSCRIPT)];
return classicResults.map((result) => ({
text: this.decodeEntities(result[3]),
duration: parseFloat(result[2]),
offset: parseFloat(result[1]),
lang,
}));
}
/**
* Decode common HTML entities in transcript text
*/
static decodeEntities(text) {
return text
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&apos;/g, "'")
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)));
}
/**
* Retrieve video id from url or string
* @param videoId video url or video id
*/
static retrieveVideoId(videoId) {
if (videoId.length === 11) {
return videoId;
}
const matchId = videoId.match(RE_YOUTUBE);
if (matchId && matchId.length) {
return matchId[1];
}
throw new YoutubeTranscriptError('Impossible to retrieve Youtube video ID.');
}
}
exports.YoutubeTranscript = YoutubeTranscript;
//^ class is kept for backward compatibility
function fetchTranscript(videoId, config) {
return YoutubeTranscript.fetchTranscript(videoId, config);
}
//# sourceMappingURL=index.js.map