extract-youtube 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/README.md +510 -0
- package/__tests__/api.test.ts +163 -0
- package/__tests__/basic-usage.ts +298 -0
- package/__tests__/formatters.test.ts +239 -0
- package/__tests__/integration.test.ts +283 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/jest.config.js +17 -0
- package/package.json +71 -0
- package/src/cli.ts +197 -0
- package/src/errors.ts +364 -0
- package/src/fetchers/transcript-list-fetcher.ts +252 -0
- package/src/fetchers/youtube-data-extractor.ts +151 -0
- package/src/formatters.ts +163 -0
- package/src/http/fetch-http-client.ts +102 -0
- package/src/index.ts +77 -0
- package/src/models/fetched-transcript.ts +110 -0
- package/src/models/index.ts +7 -0
- package/src/models/transcript-list.ts +252 -0
- package/src/models/transcript.ts +187 -0
- package/src/parsers/transcript-parser.ts +135 -0
- package/src/proxies.ts +222 -0
- package/src/types.ts +119 -0
- package/src/utils/http-error-handler.ts +32 -0
- package/src/youtube-transcript-api.ts +254 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +64 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utilities for extracting data from YouTube HTML and InnerTube API responses
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
InnerTubeData,
|
|
7
|
+
CaptionsJson,
|
|
8
|
+
PlayabilityStatus,
|
|
9
|
+
PlayabilityFailedReason
|
|
10
|
+
} from '../types';
|
|
11
|
+
import {
|
|
12
|
+
IpBlocked,
|
|
13
|
+
YouTubeDataUnparsable,
|
|
14
|
+
TranscriptsDisabled,
|
|
15
|
+
RequestBlocked,
|
|
16
|
+
AgeRestricted,
|
|
17
|
+
VideoUnavailable,
|
|
18
|
+
InvalidVideoId,
|
|
19
|
+
VideoUnplayable
|
|
20
|
+
} from '../errors';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extracts the InnerTube API key from YouTube's HTML page.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} htmlContent - The HTML content from YouTube's watch page
|
|
26
|
+
* @param {string} videoId - The video ID being processed
|
|
27
|
+
* @returns {string} The extracted API key
|
|
28
|
+
* @throws {IpBlocked} If the page contains a reCAPTCHA (IP is blocked)
|
|
29
|
+
* @throws {YouTubeDataUnparsable} If the API key cannot be found
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const html = await fetchVideoHtml(videoId);
|
|
34
|
+
* const apiKey = extractInnertubeApiKey(html, videoId);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function extractInnertubeApiKey(htmlContent: string, videoId: string): string {
|
|
38
|
+
const apiKeyPattern = /"INNERTUBE_API_KEY":\s*"([a-zA-Z0-9_-]+)"/;
|
|
39
|
+
const match = htmlContent.match(apiKeyPattern);
|
|
40
|
+
|
|
41
|
+
if (match && match.length === 2) {
|
|
42
|
+
return match[1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if IP is blocked (reCAPTCHA present)
|
|
46
|
+
if (htmlContent.includes('class="g-recaptcha"')) {
|
|
47
|
+
throw new IpBlocked(videoId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new YouTubeDataUnparsable(videoId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extracts captions JSON from YouTube's InnerTube API response.
|
|
55
|
+
*
|
|
56
|
+
* @param {InnerTubeData} innertubeData - The InnerTube API response data
|
|
57
|
+
* @param {string} videoId - The video ID being processed
|
|
58
|
+
* @returns {CaptionsJson} The extracted captions data
|
|
59
|
+
* @throws {TranscriptsDisabled} If captions are disabled for the video
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const innertubeData = await fetchInnertubeData(videoId, apiKey);
|
|
64
|
+
* const captionsJson = extractCaptionsJson(innertubeData, videoId);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function extractCaptionsJson(innertubeData: InnerTubeData, videoId: string): CaptionsJson {
|
|
68
|
+
validatePlayabilityStatus(innertubeData.playabilityStatus, videoId);
|
|
69
|
+
|
|
70
|
+
const captionsJson = innertubeData.captions?.playerCaptionsTracklistRenderer;
|
|
71
|
+
|
|
72
|
+
if (!captionsJson || !captionsJson.captionTracks) {
|
|
73
|
+
throw new TranscriptsDisabled(videoId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return captionsJson;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validates the playability status of a video and throws appropriate errors.
|
|
81
|
+
*
|
|
82
|
+
* @param {any} playabilityStatusData - The playability status data from InnerTube
|
|
83
|
+
* @param {string} videoId - The video ID being validated
|
|
84
|
+
* @throws {RequestBlocked} If YouTube detected a bot
|
|
85
|
+
* @throws {AgeRestricted} If the video is age-restricted
|
|
86
|
+
* @throws {VideoUnavailable} If the video is unavailable
|
|
87
|
+
* @throws {InvalidVideoId} If the video ID is invalid (URL was provided instead)
|
|
88
|
+
* @throws {VideoUnplayable} If the video is unplayable for any other reason
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* validatePlayabilityStatus(innertubeData.playabilityStatus, videoId);
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function validatePlayabilityStatus(playabilityStatusData: any, videoId: string): void {
|
|
96
|
+
if (!playabilityStatusData) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const playabilityStatus = playabilityStatusData.status;
|
|
101
|
+
|
|
102
|
+
// If status is OK, video is playable
|
|
103
|
+
if (playabilityStatus === PlayabilityStatus.OK || !playabilityStatus) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const reason = playabilityStatusData.reason;
|
|
108
|
+
|
|
109
|
+
// Handle LOGIN_REQUIRED status
|
|
110
|
+
if (playabilityStatus === PlayabilityStatus.LOGIN_REQUIRED) {
|
|
111
|
+
if (reason === PlayabilityFailedReason.BOT_DETECTED) {
|
|
112
|
+
throw new RequestBlocked(videoId);
|
|
113
|
+
}
|
|
114
|
+
if (reason === PlayabilityFailedReason.AGE_RESTRICTED) {
|
|
115
|
+
throw new AgeRestricted(videoId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle ERROR status
|
|
120
|
+
if (playabilityStatus === PlayabilityStatus.ERROR && reason === PlayabilityFailedReason.VIDEO_UNAVAILABLE) {
|
|
121
|
+
// Check if user provided a URL instead of video ID
|
|
122
|
+
if (videoId.startsWith('http://') || videoId.startsWith('https://')) {
|
|
123
|
+
throw new InvalidVideoId(videoId);
|
|
124
|
+
}
|
|
125
|
+
throw new VideoUnavailable(videoId);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Extract additional sub-reasons if available
|
|
129
|
+
const subReasons = extractPlayabilitySubreasons(playabilityStatusData);
|
|
130
|
+
|
|
131
|
+
throw new VideoUnplayable(videoId, reason, subReasons);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extracts sub-reasons from the playability status error screen.
|
|
136
|
+
*
|
|
137
|
+
* @private
|
|
138
|
+
* @param {any} playabilityStatusData - The playability status data
|
|
139
|
+
* @returns {string[]} Array of sub-reason messages
|
|
140
|
+
*/
|
|
141
|
+
function extractPlayabilitySubreasons(playabilityStatusData: any): string[] {
|
|
142
|
+
const runs = playabilityStatusData.errorScreen?.playerErrorMessageRenderer?.subreason?.runs;
|
|
143
|
+
|
|
144
|
+
if (!runs || !Array.isArray(runs)) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return runs
|
|
149
|
+
.map((run: any) => run.text || '')
|
|
150
|
+
.filter((text: string) => text.length > 0);
|
|
151
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview All formatter classes for transcript formatting
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FetchedTranscript } from './models/fetched-transcript';
|
|
6
|
+
import type { FetchedTranscriptSnippet } from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base abstract class for all transcript formatters.
|
|
10
|
+
*/
|
|
11
|
+
export abstract class Formatter {
|
|
12
|
+
abstract formatTranscript(transcript: FetchedTranscript, options?: any): string;
|
|
13
|
+
abstract formatTranscripts(transcripts: FetchedTranscript[], options?: any): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Formatter that converts transcripts to pretty-printed JSON format.
|
|
18
|
+
*/
|
|
19
|
+
export class PrettyPrintFormatter extends Formatter {
|
|
20
|
+
formatTranscript(transcript: FetchedTranscript): string {
|
|
21
|
+
return JSON.stringify(transcript.toRawData(), null, 2);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
formatTranscripts(transcripts: FetchedTranscript[]): string {
|
|
25
|
+
return JSON.stringify(transcripts.map(t => t.toRawData()), null, 2);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Formatter that converts transcripts to JSON format.
|
|
31
|
+
*/
|
|
32
|
+
export class JSONFormatter extends Formatter {
|
|
33
|
+
formatTranscript(transcript: FetchedTranscript, options?: { indent?: number }): string {
|
|
34
|
+
return JSON.stringify(transcript.toRawData(), null, options?.indent);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
formatTranscripts(transcripts: FetchedTranscript[], options?: { indent?: number }): string {
|
|
38
|
+
return JSON.stringify(transcripts.map(t => t.toRawData()), null, options?.indent);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Formatter that converts transcripts to plain text format with no timestamps.
|
|
44
|
+
*/
|
|
45
|
+
export class TextFormatter extends Formatter {
|
|
46
|
+
formatTranscript(transcript: FetchedTranscript): string {
|
|
47
|
+
return transcript.snippets.map(snippet => snippet.text).join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
formatTranscripts(transcripts: FetchedTranscript[]): string {
|
|
51
|
+
return transcripts.map(t => this.formatTranscript(t)).join('\n\n\n');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Base class for subtitle formatters (SRT, WebVTT).
|
|
57
|
+
*/
|
|
58
|
+
abstract class SubtitleFormatterBase extends TextFormatter {
|
|
59
|
+
protected abstract formatTimestamp(h: number, m: number, s: number, ms: number): string;
|
|
60
|
+
protected abstract formatTranscriptHeader(lines: string[]): string;
|
|
61
|
+
protected abstract formatSubtitleEntry(index: number, timeRange: string, snippet: FetchedTranscriptSnippet): string;
|
|
62
|
+
|
|
63
|
+
protected secondsToTimestamp(timeInSeconds: number): string {
|
|
64
|
+
const hours = Math.floor(timeInSeconds / 3600);
|
|
65
|
+
const remainder = timeInSeconds % 3600;
|
|
66
|
+
const minutes = Math.floor(remainder / 60);
|
|
67
|
+
const seconds = Math.floor(remainder % 60);
|
|
68
|
+
const milliseconds = Math.round((timeInSeconds - Math.floor(timeInSeconds)) * 1000);
|
|
69
|
+
|
|
70
|
+
return this.formatTimestamp(hours, minutes, seconds, milliseconds);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
formatTranscript(transcript: FetchedTranscript): string {
|
|
74
|
+
const formattedLines: string[] = [];
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < transcript.snippets.length; i++) {
|
|
77
|
+
const snippet = transcript.snippets[i];
|
|
78
|
+
const snippetEndTime = snippet.start + snippet.duration;
|
|
79
|
+
|
|
80
|
+
const actualEndTime =
|
|
81
|
+
i < transcript.snippets.length - 1 && transcript.snippets[i + 1].start < snippetEndTime
|
|
82
|
+
? transcript.snippets[i + 1].start
|
|
83
|
+
: snippetEndTime;
|
|
84
|
+
|
|
85
|
+
const timeRangeText = `${this.secondsToTimestamp(snippet.start)} --> ${this.secondsToTimestamp(actualEndTime)}`;
|
|
86
|
+
|
|
87
|
+
formattedLines.push(this.formatSubtitleEntry(i, timeRangeText, snippet));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return this.formatTranscriptHeader(formattedLines);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Formatter that converts transcripts to SRT (SubRip) format.
|
|
96
|
+
*/
|
|
97
|
+
export class SRTFormatter extends SubtitleFormatterBase {
|
|
98
|
+
protected formatTimestamp(h: number, m: number, s: number, ms: number): string {
|
|
99
|
+
const pad = (n: number, w: number) => n.toString().padStart(w, '0');
|
|
100
|
+
return `${pad(h, 2)}:${pad(m, 2)}:${pad(s, 2)},${pad(ms, 3)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected formatTranscriptHeader(lines: string[]): string {
|
|
104
|
+
return lines.join('\n\n') + '\n';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected formatSubtitleEntry(index: number, timeRange: string, snippet: FetchedTranscriptSnippet): string {
|
|
108
|
+
return `${index + 1}\n${timeRange}\n${snippet.text}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Formatter that converts transcripts to WebVTT format.
|
|
114
|
+
*/
|
|
115
|
+
export class WebVTTFormatter extends SubtitleFormatterBase {
|
|
116
|
+
protected formatTimestamp(h: number, m: number, s: number, ms: number): string {
|
|
117
|
+
const pad = (n: number, w: number) => n.toString().padStart(w, '0');
|
|
118
|
+
return `${pad(h, 2)}:${pad(m, 2)}:${pad(s, 2)}.${pad(ms, 3)}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected formatTranscriptHeader(lines: string[]): string {
|
|
122
|
+
return 'WEBVTT\n\n' + lines.join('\n\n') + '\n';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
protected formatSubtitleEntry(index: number, timeRange: string, snippet: FetchedTranscriptSnippet): string {
|
|
126
|
+
return `${timeRange}\n${snippet.text}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Raised when an unknown formatter type is requested.
|
|
132
|
+
*/
|
|
133
|
+
export class UnknownFormatterType extends Error {
|
|
134
|
+
constructor(formatterType: string) {
|
|
135
|
+
super(`Unknown formatter type: ${formatterType}. Available types: json, text, srt, webvtt, pretty`);
|
|
136
|
+
this.name = 'UnknownFormatterType';
|
|
137
|
+
Object.setPrototypeOf(this, UnknownFormatterType.prototype);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Utility class for loading formatters by type string.
|
|
143
|
+
*/
|
|
144
|
+
export class FormatterLoader {
|
|
145
|
+
load(formatterType?: string): Formatter {
|
|
146
|
+
const type = formatterType?.toLowerCase() || 'pretty';
|
|
147
|
+
|
|
148
|
+
switch (type) {
|
|
149
|
+
case 'json':
|
|
150
|
+
return new JSONFormatter();
|
|
151
|
+
case 'text':
|
|
152
|
+
return new TextFormatter();
|
|
153
|
+
case 'srt':
|
|
154
|
+
return new SRTFormatter();
|
|
155
|
+
case 'webvtt':
|
|
156
|
+
return new WebVTTFormatter();
|
|
157
|
+
case 'pretty':
|
|
158
|
+
return new PrettyPrintFormatter();
|
|
159
|
+
default:
|
|
160
|
+
throw new UnknownFormatterType(type);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HTTP client implementation using node-fetch
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fetch, { Response, RequestInit } from 'node-fetch';
|
|
6
|
+
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
7
|
+
import { HttpClient, RequestsProxyConfigDict } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Simple HTTP client implementation using node-fetch with proxy support.
|
|
11
|
+
* This client handles HTTP/HTTPS requests with configurable headers and proxy settings.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export class FetchHttpClient implements HttpClient {
|
|
16
|
+
private requestHeaders: Record<string, string> = {};
|
|
17
|
+
private proxyConfiguration: Partial<RequestsProxyConfigDict> = {};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new FetchHttpClient instance.
|
|
21
|
+
* Sets default Accept-Language header to English.
|
|
22
|
+
*/
|
|
23
|
+
constructor() {
|
|
24
|
+
this.requestHeaders['Accept-Language'] = 'en-US';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sets a custom header for all requests.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} headerName - The header name
|
|
31
|
+
* @param {string} headerValue - The header value
|
|
32
|
+
*/
|
|
33
|
+
setHeader(headerName: string, headerValue: string): void {
|
|
34
|
+
this.requestHeaders[headerName] = headerValue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Configures proxy settings for all requests.
|
|
39
|
+
*
|
|
40
|
+
* @param {RequestsProxyConfigDict} proxies - Proxy configuration dictionary
|
|
41
|
+
*/
|
|
42
|
+
setProxies(proxies: RequestsProxyConfigDict): void {
|
|
43
|
+
this.proxyConfiguration = proxies;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Performs an HTTP GET request.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} url - The URL to request
|
|
50
|
+
* @param {RequestInit} [options] - Additional request options
|
|
51
|
+
* @returns {Promise<Response>} The HTTP response
|
|
52
|
+
*/
|
|
53
|
+
async get(url: string, options?: RequestInit): Promise<Response> {
|
|
54
|
+
const proxyAgent = this.getProxyAgentForUrl(url);
|
|
55
|
+
return fetch(url, {
|
|
56
|
+
...options,
|
|
57
|
+
method: 'GET',
|
|
58
|
+
headers: { ...this.requestHeaders, ...options?.headers },
|
|
59
|
+
agent: proxyAgent
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Performs an HTTP POST request.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} url - The URL to request
|
|
67
|
+
* @param {RequestInit} [options] - Additional request options
|
|
68
|
+
* @returns {Promise<Response>} The HTTP response
|
|
69
|
+
*/
|
|
70
|
+
async post(url: string, options?: RequestInit): Promise<Response> {
|
|
71
|
+
const proxyAgent = this.getProxyAgentForUrl(url);
|
|
72
|
+
return fetch(url, {
|
|
73
|
+
...options,
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { ...this.requestHeaders, ...options?.headers },
|
|
76
|
+
agent: proxyAgent
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Gets the appropriate proxy agent for a given URL.
|
|
82
|
+
*
|
|
83
|
+
* @private
|
|
84
|
+
* @param {string} url - The URL being requested
|
|
85
|
+
* @returns {any} The proxy agent or undefined if no proxy is configured
|
|
86
|
+
*/
|
|
87
|
+
private getProxyAgentForUrl(url: string): any {
|
|
88
|
+
if (Object.keys(this.proxyConfiguration).length === 0) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const proxyUrl = url.startsWith('https://')
|
|
93
|
+
? this.proxyConfiguration.https
|
|
94
|
+
: this.proxyConfiguration.http;
|
|
95
|
+
|
|
96
|
+
if (!proxyUrl) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return new HttpsProxyAgent(proxyUrl);
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview YouTube Transcript API - A Node.js/TypeScript API to retrieve YouTube video transcripts/subtitles
|
|
3
|
+
* @module youtube-transcript-api
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Main API
|
|
7
|
+
export { YouTubeTranscriptApi } from './youtube-transcript-api';
|
|
8
|
+
|
|
9
|
+
// Transcript model classes
|
|
10
|
+
export {
|
|
11
|
+
FetchedTranscript,
|
|
12
|
+
Transcript,
|
|
13
|
+
TranscriptList
|
|
14
|
+
} from './models';
|
|
15
|
+
|
|
16
|
+
// Fetcher classes
|
|
17
|
+
export { TranscriptListFetcher } from './fetchers/transcript-list-fetcher';
|
|
18
|
+
|
|
19
|
+
// Proxy configurations
|
|
20
|
+
export {
|
|
21
|
+
ProxyConfig,
|
|
22
|
+
GenericProxyConfig,
|
|
23
|
+
WebshareProxyConfig,
|
|
24
|
+
InvalidProxyConfig
|
|
25
|
+
} from './proxies';
|
|
26
|
+
|
|
27
|
+
// Formatters
|
|
28
|
+
export {
|
|
29
|
+
Formatter,
|
|
30
|
+
JSONFormatter,
|
|
31
|
+
PrettyPrintFormatter,
|
|
32
|
+
TextFormatter,
|
|
33
|
+
SRTFormatter,
|
|
34
|
+
WebVTTFormatter,
|
|
35
|
+
FormatterLoader,
|
|
36
|
+
UnknownFormatterType
|
|
37
|
+
} from './formatters';
|
|
38
|
+
|
|
39
|
+
// Error classes
|
|
40
|
+
export {
|
|
41
|
+
YouTubeTranscriptApiException,
|
|
42
|
+
CookieError,
|
|
43
|
+
CookiePathInvalid,
|
|
44
|
+
CookieInvalid,
|
|
45
|
+
CouldNotRetrieveTranscript,
|
|
46
|
+
YouTubeDataUnparsable,
|
|
47
|
+
YouTubeRequestFailed,
|
|
48
|
+
VideoUnplayable,
|
|
49
|
+
VideoUnavailable,
|
|
50
|
+
InvalidVideoId,
|
|
51
|
+
RequestBlocked,
|
|
52
|
+
IpBlocked,
|
|
53
|
+
TranscriptsDisabled,
|
|
54
|
+
AgeRestricted,
|
|
55
|
+
NotTranslatable,
|
|
56
|
+
TranslationLanguageNotAvailable,
|
|
57
|
+
FailedToCreateConsentCookie,
|
|
58
|
+
NoTranscriptFound,
|
|
59
|
+
PoTokenRequired
|
|
60
|
+
} from './errors';
|
|
61
|
+
|
|
62
|
+
// Type definitions (enums only - interfaces are type-only and exported via .d.ts)
|
|
63
|
+
export {
|
|
64
|
+
PlayabilityStatus,
|
|
65
|
+
PlayabilityFailedReason
|
|
66
|
+
} from './types';
|
|
67
|
+
|
|
68
|
+
// Re-export types for TypeScript consumers (these are type-only exports)
|
|
69
|
+
export type {
|
|
70
|
+
FetchedTranscriptSnippet,
|
|
71
|
+
TranslationLanguage,
|
|
72
|
+
RequestsProxyConfigDict,
|
|
73
|
+
CaptionTrack,
|
|
74
|
+
CaptionsJson,
|
|
75
|
+
InnerTubeData,
|
|
76
|
+
HttpClient
|
|
77
|
+
} from './types';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview FetchedTranscript model representing a complete transcript with all snippets
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { FetchedTranscriptSnippet } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Represents a fetched transcript with all its snippets and metadata.
|
|
9
|
+
* This class is iterable, allowing you to iterate over transcript snippets.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const transcript = await api.fetch('video_id');
|
|
14
|
+
* for (const snippet of transcript) {
|
|
15
|
+
* console.log(snippet.text);
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class FetchedTranscript implements Iterable<FetchedTranscriptSnippet> {
|
|
20
|
+
/** Array of transcript snippets */
|
|
21
|
+
public readonly snippets: FetchedTranscriptSnippet[];
|
|
22
|
+
|
|
23
|
+
/** The video ID this transcript belongs to */
|
|
24
|
+
public readonly videoId: string;
|
|
25
|
+
|
|
26
|
+
/** The language name (e.g., "English") */
|
|
27
|
+
public readonly language: string;
|
|
28
|
+
|
|
29
|
+
/** The language code (e.g., "en") */
|
|
30
|
+
public readonly languageCode: string;
|
|
31
|
+
|
|
32
|
+
/** Whether this transcript was automatically generated */
|
|
33
|
+
public readonly isGenerated: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new FetchedTranscript instance.
|
|
37
|
+
*
|
|
38
|
+
* @param {FetchedTranscriptSnippet[]} snippets - Array of transcript snippets
|
|
39
|
+
* @param {string} videoId - The video ID
|
|
40
|
+
* @param {string} language - The language name
|
|
41
|
+
* @param {string} languageCode - The language code
|
|
42
|
+
* @param {boolean} isGenerated - Whether the transcript is auto-generated
|
|
43
|
+
*/
|
|
44
|
+
constructor(
|
|
45
|
+
snippets: FetchedTranscriptSnippet[],
|
|
46
|
+
videoId: string,
|
|
47
|
+
language: string,
|
|
48
|
+
languageCode: string,
|
|
49
|
+
isGenerated: boolean
|
|
50
|
+
) {
|
|
51
|
+
this.snippets = snippets;
|
|
52
|
+
this.videoId = videoId;
|
|
53
|
+
this.language = language;
|
|
54
|
+
this.languageCode = languageCode;
|
|
55
|
+
this.isGenerated = isGenerated;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Makes the transcript iterable, allowing for...of loops.
|
|
60
|
+
*
|
|
61
|
+
* @returns {Iterator<FetchedTranscriptSnippet>} Iterator over the snippets
|
|
62
|
+
*/
|
|
63
|
+
[Symbol.iterator](): Iterator<FetchedTranscriptSnippet> {
|
|
64
|
+
return this.snippets[Symbol.iterator]();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Gets a snippet by index.
|
|
69
|
+
*
|
|
70
|
+
* @param {number} index - The index of the snippet
|
|
71
|
+
* @returns {FetchedTranscriptSnippet} The snippet at the given index
|
|
72
|
+
*/
|
|
73
|
+
getSnippetAtIndex(index: number): FetchedTranscriptSnippet {
|
|
74
|
+
return this.snippets[index];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Alias for getSnippetAtIndex() for backward compatibility.
|
|
79
|
+
*
|
|
80
|
+
* @deprecated Use getSnippetAtIndex() instead
|
|
81
|
+
* @param {number} index - The index of the snippet
|
|
82
|
+
* @returns {FetchedTranscriptSnippet} The snippet at the given index
|
|
83
|
+
*/
|
|
84
|
+
get(index: number): FetchedTranscriptSnippet {
|
|
85
|
+
return this.getSnippetAtIndex(index);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Gets the total number of snippets in the transcript.
|
|
90
|
+
*
|
|
91
|
+
* @returns {number} The number of snippets
|
|
92
|
+
*/
|
|
93
|
+
get length(): number {
|
|
94
|
+
return this.snippets.length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Converts the transcript to raw data format (array of objects).
|
|
99
|
+
* This is useful for serialization or when you need plain data objects.
|
|
100
|
+
*
|
|
101
|
+
* @returns {Array<{text: string, start: number, duration: number}>} Raw transcript data
|
|
102
|
+
*/
|
|
103
|
+
toRawData(): Array<{ text: string; start: number; duration: number }> {
|
|
104
|
+
return this.snippets.map(snippet => ({
|
|
105
|
+
text: snippet.text,
|
|
106
|
+
start: snippet.start,
|
|
107
|
+
duration: snippet.duration
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
}
|