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
package/src/proxies.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Proxy configuration classes for the YouTube Transcript API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { RequestsProxyConfigDict } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Exception for invalid proxy configurations
|
|
9
|
+
*/
|
|
10
|
+
export class InvalidProxyConfig extends Error {
|
|
11
|
+
constructor(message: string) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'InvalidProxyConfig';
|
|
14
|
+
Object.setPrototypeOf(this, InvalidProxyConfig.prototype);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Base class for all proxy configurations.
|
|
20
|
+
* Any proxy config can be used as long as it can be converted to a RequestsProxyConfigDict.
|
|
21
|
+
*/
|
|
22
|
+
export abstract class ProxyConfig {
|
|
23
|
+
/**
|
|
24
|
+
* Converts this proxy config to a dictionary that can be used by HTTP clients.
|
|
25
|
+
* @returns {RequestsProxyConfigDict} The proxy configuration dictionary
|
|
26
|
+
*/
|
|
27
|
+
abstract toRequestsDict(): RequestsProxyConfigDict;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* If you are using rotating proxies, it can be useful to prevent the HTTP
|
|
31
|
+
* client from keeping TCP connections alive, as your IP won't be rotated on
|
|
32
|
+
* every request if your connection stays open.
|
|
33
|
+
* @returns {boolean} Whether to prevent keeping connections alive
|
|
34
|
+
*/
|
|
35
|
+
get preventKeepingConnectionsAlive(): boolean {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Defines how many times we should retry if a request is blocked. When using
|
|
41
|
+
* rotating residential proxies with a large IP pool it can make sense to retry a
|
|
42
|
+
* couple of times when a blocked IP is encountered, since a retry will trigger
|
|
43
|
+
* an IP rotation and the next IP might not be blocked.
|
|
44
|
+
* @returns {number} Number of retries when blocked
|
|
45
|
+
*/
|
|
46
|
+
get retriesWhenBlocked(): number {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generic proxy configuration that can be used to set up any HTTP/HTTPS/SOCKS proxy.
|
|
53
|
+
*
|
|
54
|
+
* If only an HTTP or an HTTPS proxy is provided, it will be used for both types of
|
|
55
|
+
* connections. However, you will have to provide at least one of the two.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const proxyConfig = new GenericProxyConfig({
|
|
60
|
+
* httpUrl: 'http://user:pass@proxy.example.com:8080',
|
|
61
|
+
* httpsUrl: 'https://user:pass@proxy.example.com:8080'
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export class GenericProxyConfig extends ProxyConfig {
|
|
66
|
+
protected _httpUrl: string | null;
|
|
67
|
+
protected _httpsUrl: string | null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a generic proxy configuration.
|
|
71
|
+
* If only an HTTP or an HTTPS proxy is provided, it will be used for both types of
|
|
72
|
+
* connections. However, you will have to provide at least one of the two.
|
|
73
|
+
*
|
|
74
|
+
* @param {Object} options - Proxy configuration options
|
|
75
|
+
* @param {string} [options.httpUrl] - The proxy URL used for HTTP requests. Defaults to httpsUrl if not provided.
|
|
76
|
+
* @param {string} [options.httpsUrl] - The proxy URL used for HTTPS requests. Defaults to httpUrl if not provided.
|
|
77
|
+
* @throws {InvalidProxyConfig} If neither httpUrl nor httpsUrl is provided
|
|
78
|
+
*/
|
|
79
|
+
constructor(options: { httpUrl?: string; httpsUrl?: string }) {
|
|
80
|
+
super();
|
|
81
|
+
const { httpUrl, httpsUrl } = options;
|
|
82
|
+
|
|
83
|
+
if (!httpUrl && !httpsUrl) {
|
|
84
|
+
throw new InvalidProxyConfig(
|
|
85
|
+
'GenericProxyConfig requires you to define at least one of the two: http or https'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this._httpUrl = httpUrl || null;
|
|
90
|
+
this._httpsUrl = httpsUrl || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get httpUrl(): string | null {
|
|
94
|
+
return this._httpUrl;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get httpsUrl(): string | null {
|
|
98
|
+
return this._httpsUrl;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
toRequestsDict(): RequestsProxyConfigDict {
|
|
102
|
+
return {
|
|
103
|
+
http: this.httpUrl || this.httpsUrl!,
|
|
104
|
+
https: this.httpsUrl || this.httpUrl!
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Webshare proxy configuration for rotating residential proxies.
|
|
111
|
+
*
|
|
112
|
+
* Webshare is a provider offering rotating residential proxies, which is the
|
|
113
|
+
* most reliable way to work around being blocked by YouTube.
|
|
114
|
+
*
|
|
115
|
+
* If you don't have a Webshare account yet, you will have to create one
|
|
116
|
+
* at https://www.webshare.io/?referral_code=w0xno53eb50g and purchase a "Residential"
|
|
117
|
+
* proxy package that suits your workload (make sure NOT to purchase "Proxy Server"
|
|
118
|
+
* or "Static Residential"!).
|
|
119
|
+
*
|
|
120
|
+
* Once you have created an account you only need the "Proxy Username" and
|
|
121
|
+
* "Proxy Password" that you can find in your Webshare settings to set up this config.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const proxyConfig = new WebshareProxyConfig({
|
|
126
|
+
* proxyUsername: 'your-username',
|
|
127
|
+
* proxyPassword: 'your-password',
|
|
128
|
+
* filterIpLocations: ['us', 'de']
|
|
129
|
+
* });
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export class WebshareProxyConfig extends GenericProxyConfig {
|
|
133
|
+
public static readonly DEFAULT_DOMAIN_NAME = 'p.webshare.io';
|
|
134
|
+
public static readonly DEFAULT_PORT = 80;
|
|
135
|
+
|
|
136
|
+
public readonly proxyUsername: string;
|
|
137
|
+
public readonly proxyPassword: string;
|
|
138
|
+
public readonly domainName: string;
|
|
139
|
+
public readonly proxyPort: number;
|
|
140
|
+
private readonly filterIpLocations: string[];
|
|
141
|
+
private readonly _retriesWhenBlocked: number;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Creates a Webshare proxy configuration.
|
|
145
|
+
*
|
|
146
|
+
* Once you have created a Webshare account and purchased a "Residential" package
|
|
147
|
+
* (make sure NOT to purchase "Proxy Server" or "Static Residential"!), this config
|
|
148
|
+
* class allows you to easily use it by defaulting to the most reliable proxy settings
|
|
149
|
+
* (rotating residential proxies).
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} options - Webshare configuration options
|
|
152
|
+
* @param {string} options.proxyUsername - "Proxy Username" found at https://dashboard.webshare.io/proxy/settings
|
|
153
|
+
* @param {string} options.proxyPassword - "Proxy Password" found at https://dashboard.webshare.io/proxy/settings
|
|
154
|
+
* @param {string[]} [options.filterIpLocations] - If you want to limit the pool of IPs to specific countries,
|
|
155
|
+
* provide a list of location codes (e.g., ['us', 'de']). This can reduce latency and work around location-based restrictions.
|
|
156
|
+
* @param {number} [options.retriesWhenBlocked=10] - How many times to retry if a request is blocked
|
|
157
|
+
* @param {string} [options.domainName] - Custom domain name (defaults to p.webshare.io)
|
|
158
|
+
* @param {number} [options.proxyPort] - Custom proxy port (defaults to 80)
|
|
159
|
+
*/
|
|
160
|
+
constructor(options: {
|
|
161
|
+
proxyUsername: string;
|
|
162
|
+
proxyPassword: string;
|
|
163
|
+
filterIpLocations?: string[];
|
|
164
|
+
retriesWhenBlocked?: number;
|
|
165
|
+
domainName?: string;
|
|
166
|
+
proxyPort?: number;
|
|
167
|
+
}) {
|
|
168
|
+
super({ httpUrl: 'placeholder' });
|
|
169
|
+
|
|
170
|
+
this.proxyUsername = options.proxyUsername;
|
|
171
|
+
this.proxyPassword = options.proxyPassword;
|
|
172
|
+
this.domainName = options.domainName || WebshareProxyConfig.DEFAULT_DOMAIN_NAME;
|
|
173
|
+
this.proxyPort = options.proxyPort || WebshareProxyConfig.DEFAULT_PORT;
|
|
174
|
+
this.filterIpLocations = options.filterIpLocations || [];
|
|
175
|
+
this._retriesWhenBlocked = options.retriesWhenBlocked ?? 10;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Gets the full proxy URL with location filtering and rotation settings.
|
|
180
|
+
* @returns {string} The complete proxy URL
|
|
181
|
+
*/
|
|
182
|
+
get url(): string {
|
|
183
|
+
const locationCodes = this.filterIpLocations
|
|
184
|
+
.map(code => `-${code.toUpperCase()}`)
|
|
185
|
+
.join('');
|
|
186
|
+
|
|
187
|
+
let username = this.proxyUsername;
|
|
188
|
+
const suffix = '-rotate';
|
|
189
|
+
|
|
190
|
+
if (username.endsWith(suffix)) {
|
|
191
|
+
username = username.slice(0, -suffix.length);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
`http://${username}${locationCodes}${suffix}:${this.proxyPassword}` +
|
|
196
|
+
`@${this.domainName}:${this.proxyPort}/`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get httpUrl(): string {
|
|
201
|
+
return this.url;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
get httpsUrl(): string {
|
|
205
|
+
return this.url;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
toRequestsDict(): RequestsProxyConfigDict {
|
|
209
|
+
return {
|
|
210
|
+
http: this.url,
|
|
211
|
+
https: this.url
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
get preventKeepingConnectionsAlive(): boolean {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get retriesWhenBlocked(): number {
|
|
220
|
+
return this._retriesWhenBlocked;
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Type definitions for the YouTube Transcript API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Response, RequestInit } from 'node-fetch';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Represents a single snippet/cue of a transcript
|
|
9
|
+
*/
|
|
10
|
+
export interface FetchedTranscriptSnippet {
|
|
11
|
+
/** The text content of this transcript snippet */
|
|
12
|
+
text: string;
|
|
13
|
+
/** The timestamp at which this transcript snippet appears on screen in seconds */
|
|
14
|
+
start: number;
|
|
15
|
+
/**
|
|
16
|
+
* The duration of how long the snippet stays on screen in seconds.
|
|
17
|
+
* Note: This is not the duration of the transcribed speech, but how long
|
|
18
|
+
* the snippet stays on screen. Therefore, there can be overlaps between snippets!
|
|
19
|
+
*/
|
|
20
|
+
duration: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Translation language information
|
|
25
|
+
*/
|
|
26
|
+
export interface TranslationLanguage {
|
|
27
|
+
/** The name of the language */
|
|
28
|
+
language: string;
|
|
29
|
+
/** The language code (e.g., 'en', 'de', 'fr') */
|
|
30
|
+
language_code: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Playability status enum values
|
|
35
|
+
*/
|
|
36
|
+
export enum PlayabilityStatus {
|
|
37
|
+
OK = 'OK',
|
|
38
|
+
ERROR = 'ERROR',
|
|
39
|
+
LOGIN_REQUIRED = 'LOGIN_REQUIRED'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Playability failed reason enum values
|
|
44
|
+
*/
|
|
45
|
+
export enum PlayabilityFailedReason {
|
|
46
|
+
BOT_DETECTED = "Sign in to confirm you're not a bot",
|
|
47
|
+
AGE_RESTRICTED = 'This video may be inappropriate for some users.',
|
|
48
|
+
VIDEO_UNAVAILABLE = 'This video is unavailable'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Proxy configuration dictionary for HTTP clients
|
|
53
|
+
*/
|
|
54
|
+
export interface RequestsProxyConfigDict {
|
|
55
|
+
http: string;
|
|
56
|
+
https: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Caption track data from YouTube API
|
|
61
|
+
*/
|
|
62
|
+
export interface CaptionTrack {
|
|
63
|
+
baseUrl: string;
|
|
64
|
+
name: {
|
|
65
|
+
runs: Array<{ text: string }>;
|
|
66
|
+
};
|
|
67
|
+
languageCode: string;
|
|
68
|
+
kind?: string;
|
|
69
|
+
isTranslatable?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Captions JSON data from YouTube
|
|
74
|
+
*/
|
|
75
|
+
export interface CaptionsJson {
|
|
76
|
+
captionTracks: CaptionTrack[];
|
|
77
|
+
translationLanguages?: Array<{
|
|
78
|
+
languageName: {
|
|
79
|
+
runs: Array<{ text: string }>;
|
|
80
|
+
};
|
|
81
|
+
languageCode: string;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* YouTube InnerTube API response
|
|
87
|
+
*/
|
|
88
|
+
export interface InnerTubeData {
|
|
89
|
+
playabilityStatus?: {
|
|
90
|
+
status?: string;
|
|
91
|
+
reason?: string;
|
|
92
|
+
errorScreen?: {
|
|
93
|
+
playerErrorMessageRenderer?: {
|
|
94
|
+
subreason?: {
|
|
95
|
+
runs?: Array<{ text?: string }>;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
captions?: {
|
|
101
|
+
playerCaptionsTracklistRenderer?: CaptionsJson;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* HTTP client interface with fetch-like API
|
|
107
|
+
*/
|
|
108
|
+
export interface HttpClient {
|
|
109
|
+
get(url: string, options?: RequestInit): Promise<Response>;
|
|
110
|
+
post(url: string, options?: RequestInit): Promise<Response>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Cookie storage interface
|
|
115
|
+
*/
|
|
116
|
+
export interface CookieJar {
|
|
117
|
+
set(name: string, value: string, domain: string): void;
|
|
118
|
+
get(name: string): string | undefined;
|
|
119
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HTTP error handling utilities for YouTube API requests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Response } from 'node-fetch';
|
|
6
|
+
import { IpBlocked, YouTubeRequestFailed } from '../errors';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checks an HTTP response and throws appropriate errors if the request failed.
|
|
10
|
+
*
|
|
11
|
+
* @param {Response} response - The HTTP response to check
|
|
12
|
+
* @param {string} videoId - The video ID associated with the request
|
|
13
|
+
* @throws {IpBlocked} If status is 429 (Too Many Requests)
|
|
14
|
+
* @throws {YouTubeRequestFailed} If status indicates an error
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const response = await httpClient.get(url);
|
|
19
|
+
* handleHttpErrors(response, videoId);
|
|
20
|
+
* // If we reach here, the request was successful
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function handleHttpErrors(response: Response, videoId: string): void {
|
|
24
|
+
if (response.status === 429) {
|
|
25
|
+
throw new IpBlocked(videoId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
30
|
+
throw new YouTubeRequestFailed(videoId, new Error(errorMessage));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main API class for the YouTube Transcript API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ProxyConfig } from './proxies';
|
|
6
|
+
import { FetchedTranscript, TranscriptList } from './models';
|
|
7
|
+
import { TranscriptListFetcher } from './fetchers/transcript-list-fetcher';
|
|
8
|
+
import { HttpClient } from './types';
|
|
9
|
+
import { FetchHttpClient } from './http/fetch-http-client';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Main API class for retrieving YouTube transcripts.
|
|
13
|
+
* This is the primary entry point for the library.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // Basic usage
|
|
18
|
+
* const api = new YouTubeTranscriptApi();
|
|
19
|
+
* const transcript = await api.fetchTranscript('video_id');
|
|
20
|
+
*
|
|
21
|
+
* // With language preference
|
|
22
|
+
* const transcript = await api.fetchTranscript('video_id', { languages: ['de', 'en'] });
|
|
23
|
+
*
|
|
24
|
+
* // With proxy
|
|
25
|
+
* import { GenericProxyConfig } from 'youtube-transcript-api';
|
|
26
|
+
* const api = new YouTubeTranscriptApi({
|
|
27
|
+
* proxyConfig: new GenericProxyConfig({
|
|
28
|
+
* httpUrl: 'http://user:pass@proxy.example.com:8080'
|
|
29
|
+
* })
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class YouTubeTranscriptApi {
|
|
34
|
+
private transcriptListFetcher: TranscriptListFetcher;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a new YouTubeTranscriptApi instance.
|
|
38
|
+
*
|
|
39
|
+
* Note on thread-safety: As this class initializes an HTTP client,
|
|
40
|
+
* it is not thread-safe. Make sure to initialize an instance per
|
|
41
|
+
* thread if used in a multi-threading scenario!
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} [options] - Configuration options
|
|
44
|
+
* @param {ProxyConfig} [options.proxyConfig] - An optional ProxyConfig object defining proxies
|
|
45
|
+
* used for all network requests. This can be used to work around your IP being blocked
|
|
46
|
+
* by YouTube, as described in the "Working around IP bans" section of the README.
|
|
47
|
+
* @param {HttpClient} [options.httpClient] - You can optionally pass in a custom HTTP client
|
|
48
|
+
* if you want to share state between different instances of YouTubeTranscriptApi or
|
|
49
|
+
* customize request behavior.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* // Basic usage
|
|
54
|
+
* const api = new YouTubeTranscriptApi();
|
|
55
|
+
*
|
|
56
|
+
* // With Webshare proxy
|
|
57
|
+
* import { WebshareProxyConfig } from 'youtube-transcript-api';
|
|
58
|
+
* const api = new YouTubeTranscriptApi({
|
|
59
|
+
* proxyConfig: new WebshareProxyConfig({
|
|
60
|
+
* proxyUsername: 'your-username',
|
|
61
|
+
* proxyPassword: 'your-password',
|
|
62
|
+
* filterIpLocations: ['us', 'de']
|
|
63
|
+
* })
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* // With generic proxy
|
|
67
|
+
* import { GenericProxyConfig } from 'youtube-transcript-api';
|
|
68
|
+
* const api = new YouTubeTranscriptApi({
|
|
69
|
+
* proxyConfig: new GenericProxyConfig({
|
|
70
|
+
* httpUrl: 'http://user:pass@proxy.example.com:8080',
|
|
71
|
+
* httpsUrl: 'https://user:pass@proxy.example.com:8080'
|
|
72
|
+
* })
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
constructor(options?: { proxyConfig?: ProxyConfig; httpClient?: HttpClient }) {
|
|
77
|
+
const httpClient = this.initializeHttpClient(options);
|
|
78
|
+
this.configureHttpClientWithProxy(httpClient, options?.proxyConfig);
|
|
79
|
+
this.transcriptListFetcher = new TranscriptListFetcher(httpClient, options?.proxyConfig || null);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Initializes the HTTP client from options or creates a default one.
|
|
84
|
+
*
|
|
85
|
+
* @private
|
|
86
|
+
* @param {Object} [options] - Configuration options
|
|
87
|
+
* @returns {HttpClient} The initialized HTTP client
|
|
88
|
+
*/
|
|
89
|
+
private initializeHttpClient(options?: { httpClient?: HttpClient }): HttpClient {
|
|
90
|
+
return options?.httpClient || new FetchHttpClient();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Configures the HTTP client with proxy settings if provided.
|
|
95
|
+
*
|
|
96
|
+
* @private
|
|
97
|
+
* @param {HttpClient} httpClient - The HTTP client to configure
|
|
98
|
+
* @param {ProxyConfig} [proxyConfig] - Optional proxy configuration
|
|
99
|
+
*/
|
|
100
|
+
private configureHttpClientWithProxy(httpClient: HttpClient, proxyConfig?: ProxyConfig): void {
|
|
101
|
+
if (!proxyConfig || !(httpClient instanceof FetchHttpClient)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const proxyDictionary = proxyConfig.toRequestsDict();
|
|
106
|
+
httpClient.setProxies(proxyDictionary);
|
|
107
|
+
|
|
108
|
+
if (proxyConfig.preventKeepingConnectionsAlive) {
|
|
109
|
+
httpClient.setHeader('Connection', 'close');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Note: Retry logic for 429 status codes with configured retries
|
|
113
|
+
// would need to be implemented in the HTTP client or fetcher
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Retrieves the transcript for a single video.
|
|
118
|
+
* This is a shortcut for calling:
|
|
119
|
+
* `api.listTranscripts(videoId).then(list => list.findTranscript(languages)).then(t => t.fetch())`
|
|
120
|
+
*
|
|
121
|
+
* @param {string} videoId - The ID of the video you want to retrieve the transcript for.
|
|
122
|
+
* Make sure that this is the actual ID, NOT the full URL to the video!
|
|
123
|
+
* @param {Object} [options] - Fetch options
|
|
124
|
+
* @param {string[]} [options.languages=['en']] - A list of language codes in descending priority.
|
|
125
|
+
* For example, if this is set to ['de', 'en'] it will first try to fetch the german
|
|
126
|
+
* transcript (de) and then fetch the english transcript (en) if it fails to do so.
|
|
127
|
+
* @param {boolean} [options.preserveFormatting=false] - Whether to keep select HTML text formatting
|
|
128
|
+
* @returns {Promise<FetchedTranscript>} The fetched transcript
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* const api = new YouTubeTranscriptApi();
|
|
133
|
+
*
|
|
134
|
+
* // Fetch English transcript
|
|
135
|
+
* const transcript = await api.fetchTranscript('dQw4w9WgXcQ');
|
|
136
|
+
*
|
|
137
|
+
* // Fetch with language preference
|
|
138
|
+
* const transcript = await api.fetchTranscript('dQw4w9WgXcQ', {
|
|
139
|
+
* languages: ['de', 'en']
|
|
140
|
+
* });
|
|
141
|
+
*
|
|
142
|
+
* // Preserve HTML formatting
|
|
143
|
+
* const transcript = await api.fetchTranscript('dQw4w9WgXcQ', {
|
|
144
|
+
* preserveFormatting: true
|
|
145
|
+
* });
|
|
146
|
+
*
|
|
147
|
+
* // Iterate over snippets
|
|
148
|
+
* for (const snippet of transcript) {
|
|
149
|
+
* console.log(`${snippet.start}s: ${snippet.text}`);
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
async fetchTranscript(
|
|
154
|
+
videoId: string,
|
|
155
|
+
options?: {
|
|
156
|
+
languages?: string[];
|
|
157
|
+
preserveFormatting?: boolean;
|
|
158
|
+
}
|
|
159
|
+
): Promise<FetchedTranscript> {
|
|
160
|
+
const preferredLanguages = options?.languages || ['en'];
|
|
161
|
+
const preserveFormatting = options?.preserveFormatting || false;
|
|
162
|
+
|
|
163
|
+
const transcriptList = await this.listTranscripts(videoId);
|
|
164
|
+
const transcript = transcriptList.findTranscript(preferredLanguages);
|
|
165
|
+
return transcript.fetch(preserveFormatting);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Retrieves the list of transcripts which are available for a given video.
|
|
170
|
+
* It returns a TranscriptList object which is iterable and provides methods to
|
|
171
|
+
* filter the list of transcripts for specific languages.
|
|
172
|
+
*
|
|
173
|
+
* While iterating over the TranscriptList, the individual transcripts are
|
|
174
|
+
* represented by Transcript objects, which provide metadata and can either
|
|
175
|
+
* be fetched by calling transcript.fetch() or translated by calling
|
|
176
|
+
* transcript.translate('en').
|
|
177
|
+
*
|
|
178
|
+
* @param {string} videoId - The ID of the video you want to retrieve the transcript for.
|
|
179
|
+
* Make sure that this is the actual ID, NOT the full URL to the video!
|
|
180
|
+
* @returns {Promise<TranscriptList>} A list of available transcripts
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* const api = new YouTubeTranscriptApi();
|
|
185
|
+
*
|
|
186
|
+
* // Retrieve available transcripts
|
|
187
|
+
* const transcriptList = await api.listTranscripts('video_id');
|
|
188
|
+
*
|
|
189
|
+
* // Iterate over all available transcripts
|
|
190
|
+
* for (const transcript of transcriptList) {
|
|
191
|
+
* console.log(
|
|
192
|
+
* transcript.videoId,
|
|
193
|
+
* transcript.language,
|
|
194
|
+
* transcript.languageCode,
|
|
195
|
+
* transcript.isGenerated,
|
|
196
|
+
* transcript.isTranslatable,
|
|
197
|
+
* transcript.translationLanguages
|
|
198
|
+
* );
|
|
199
|
+
*
|
|
200
|
+
* // Fetch the actual transcript data
|
|
201
|
+
* const fetched = await transcript.fetch();
|
|
202
|
+
* console.log(fetched);
|
|
203
|
+
*
|
|
204
|
+
* // Translate the transcript
|
|
205
|
+
* if (transcript.isTranslatable) {
|
|
206
|
+
* const translated = transcript.translate('de');
|
|
207
|
+
* const fetchedTranslated = await translated.fetch();
|
|
208
|
+
* console.log(fetchedTranslated);
|
|
209
|
+
* }
|
|
210
|
+
* }
|
|
211
|
+
*
|
|
212
|
+
* // Filter for specific language
|
|
213
|
+
* const transcript = transcriptList.findTranscript(['de', 'en']);
|
|
214
|
+
*
|
|
215
|
+
* // Filter for manually created transcripts
|
|
216
|
+
* const manual = transcriptList.findManuallyCreatedTranscript(['de', 'en']);
|
|
217
|
+
*
|
|
218
|
+
* // Filter for automatically generated transcripts
|
|
219
|
+
* const generated = transcriptList.findGeneratedTranscript(['de', 'en']);
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
async listTranscripts(videoId: string): Promise<TranscriptList> {
|
|
223
|
+
return this.transcriptListFetcher.fetchTranscriptList(videoId);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Alias for fetchTranscript() for backward compatibility.
|
|
228
|
+
*
|
|
229
|
+
* @deprecated Use fetchTranscript() instead
|
|
230
|
+
* @param {string} videoId - The video ID
|
|
231
|
+
* @param {Object} [options] - Fetch options
|
|
232
|
+
* @returns {Promise<FetchedTranscript>} The fetched transcript
|
|
233
|
+
*/
|
|
234
|
+
async fetch(
|
|
235
|
+
videoId: string,
|
|
236
|
+
options?: {
|
|
237
|
+
languages?: string[];
|
|
238
|
+
preserveFormatting?: boolean;
|
|
239
|
+
}
|
|
240
|
+
): Promise<FetchedTranscript> {
|
|
241
|
+
return this.fetchTranscript(videoId, options);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Alias for listTranscripts() for backward compatibility.
|
|
246
|
+
*
|
|
247
|
+
* @deprecated Use listTranscripts() instead
|
|
248
|
+
* @param {string} videoId - The video ID
|
|
249
|
+
* @returns {Promise<TranscriptList>} A list of available transcripts
|
|
250
|
+
*/
|
|
251
|
+
async list(videoId: string): Promise<TranscriptList> {
|
|
252
|
+
return this.listTranscripts(videoId);
|
|
253
|
+
}
|
|
254
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"moduleResolution": "node",
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*", "__tests__"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "__tests__"]
|
|
20
|
+
}
|