youtube-transcript-plus 1.1.1 → 1.2.0
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/README.md +18 -9
- package/dist/__tests__/cache/fs-cache.test.d.ts +1 -0
- package/dist/__tests__/cache/in-memory-cache.test.d.ts +1 -0
- package/dist/__tests__/index.test.d.ts +1 -0
- package/dist/__tests__/integration.test.d.ts +1 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/cache/fs-cache.d.ts +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/youtube-transcript-plus.js +32 -8
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -169,7 +169,7 @@ fetchTranscript('videoId_or_URL', {
|
|
|
169
169
|
|
|
170
170
|
### Error Handling
|
|
171
171
|
|
|
172
|
-
The library throws specific errors for different failure scenarios.
|
|
172
|
+
The library throws specific errors for different failure scenarios. Each error includes a `videoId` property for programmatic handling.
|
|
173
173
|
|
|
174
174
|
```javascript
|
|
175
175
|
import {
|
|
@@ -183,13 +183,13 @@ fetchTranscript('videoId_or_URL')
|
|
|
183
183
|
.then(console.log)
|
|
184
184
|
.catch((error) => {
|
|
185
185
|
if (error instanceof YoutubeTranscriptVideoUnavailableError) {
|
|
186
|
-
console.error('Video is unavailable:', error.
|
|
186
|
+
console.error('Video is unavailable:', error.videoId);
|
|
187
187
|
} else if (error instanceof YoutubeTranscriptDisabledError) {
|
|
188
|
-
console.error('Transcripts are disabled
|
|
188
|
+
console.error('Transcripts are disabled:', error.videoId);
|
|
189
189
|
} else if (error instanceof YoutubeTranscriptNotAvailableError) {
|
|
190
|
-
console.error('No transcript available:', error.
|
|
190
|
+
console.error('No transcript available:', error.videoId);
|
|
191
191
|
} else if (error instanceof YoutubeTranscriptNotAvailableLanguageError) {
|
|
192
|
-
console.error('
|
|
192
|
+
console.error('Language not available:', error.lang, error.availableLangs);
|
|
193
193
|
} else {
|
|
194
194
|
console.error('An unexpected error occurred:', error.message);
|
|
195
195
|
}
|
|
@@ -209,6 +209,14 @@ The repository includes several example files in the `example/` directory to dem
|
|
|
209
209
|
|
|
210
210
|
These examples can be found in the `example/` directory of the repository.
|
|
211
211
|
|
|
212
|
+
### TypeScript Types
|
|
213
|
+
|
|
214
|
+
All types are exported for TypeScript consumers:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import type { TranscriptConfig, TranscriptResponse, FetchParams, CacheStrategy } from 'youtube-transcript-plus';
|
|
218
|
+
```
|
|
219
|
+
|
|
212
220
|
### API
|
|
213
221
|
|
|
214
222
|
### `fetchTranscript(videoId: string, config?: TranscriptConfig)`
|
|
@@ -237,10 +245,11 @@ Returns a `Promise<TranscriptResponse[]>` where each item in the array represent
|
|
|
237
245
|
|
|
238
246
|
The library throws the following errors:
|
|
239
247
|
|
|
240
|
-
- **`YoutubeTranscriptVideoUnavailableError`**: The video is unavailable or has been removed.
|
|
241
|
-
- **`YoutubeTranscriptDisabledError`**: Transcripts are disabled for the video.
|
|
242
|
-
- **`YoutubeTranscriptNotAvailableError`**: No transcript is available for the video.
|
|
243
|
-
- **`YoutubeTranscriptNotAvailableLanguageError`**: The transcript is not available in the specified language.
|
|
248
|
+
- **`YoutubeTranscriptVideoUnavailableError`**: The video is unavailable or has been removed. Properties: `videoId`.
|
|
249
|
+
- **`YoutubeTranscriptDisabledError`**: Transcripts are disabled for the video. Properties: `videoId`.
|
|
250
|
+
- **`YoutubeTranscriptNotAvailableError`**: No transcript is available for the video. Properties: `videoId`.
|
|
251
|
+
- **`YoutubeTranscriptNotAvailableLanguageError`**: The transcript is not available in the specified language. Properties: `videoId`, `lang`, `availableLangs`.
|
|
252
|
+
- **`YoutubeTranscriptTooManyRequestError`**: YouTube is rate-limiting requests from your IP.
|
|
244
253
|
- **`YoutubeTranscriptInvalidVideoIdError`**: The provided video ID or URL is invalid.
|
|
245
254
|
|
|
246
255
|
## License
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cache/fs-cache.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { CacheStrategy } from '../types';
|
|
|
2
2
|
export declare class FsCache implements CacheStrategy {
|
|
3
3
|
private cacheDir;
|
|
4
4
|
private defaultTTL;
|
|
5
|
+
private ready;
|
|
5
6
|
constructor(cacheDir?: string, defaultTTL?: number);
|
|
6
7
|
get(key: string): Promise<string | null>;
|
|
7
8
|
set(key: string, value: string, ttl?: number): Promise<void>;
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,15 +2,21 @@ export declare class YoutubeTranscriptTooManyRequestError extends Error {
|
|
|
2
2
|
constructor();
|
|
3
3
|
}
|
|
4
4
|
export declare class YoutubeTranscriptVideoUnavailableError extends Error {
|
|
5
|
+
readonly videoId: string;
|
|
5
6
|
constructor(videoId: string);
|
|
6
7
|
}
|
|
7
8
|
export declare class YoutubeTranscriptDisabledError extends Error {
|
|
9
|
+
readonly videoId: string;
|
|
8
10
|
constructor(videoId: string);
|
|
9
11
|
}
|
|
10
12
|
export declare class YoutubeTranscriptNotAvailableError extends Error {
|
|
13
|
+
readonly videoId: string;
|
|
11
14
|
constructor(videoId: string);
|
|
12
15
|
}
|
|
13
16
|
export declare class YoutubeTranscriptNotAvailableLanguageError extends Error {
|
|
17
|
+
readonly videoId: string;
|
|
18
|
+
readonly lang: string;
|
|
19
|
+
readonly availableLangs: string[];
|
|
14
20
|
constructor(lang: string, availableLangs: string[], videoId: string);
|
|
15
21
|
}
|
|
16
22
|
export declare class YoutubeTranscriptInvalidVideoIdError extends Error {
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare class YoutubeTranscript {
|
|
|
11
11
|
fetchTranscript(videoId: string): Promise<TranscriptResponse[]>;
|
|
12
12
|
static fetchTranscript(videoId: string, config?: TranscriptConfig): Promise<TranscriptResponse[]>;
|
|
13
13
|
}
|
|
14
|
-
export type { CacheStrategy } from './types';
|
|
14
|
+
export type { CacheStrategy, TranscriptConfig, TranscriptResponse, FetchParams } from './types';
|
|
15
15
|
export { InMemoryCache, FsCache } from './cache';
|
|
16
16
|
export * from './errors';
|
|
17
17
|
export declare const fetchTranscript: typeof YoutubeTranscript.fetchTranscript;
|
package/dist/types.d.ts
CHANGED
package/dist/utils.d.ts
CHANGED
|
@@ -48,24 +48,30 @@ class YoutubeTranscriptVideoUnavailableError extends Error {
|
|
|
48
48
|
constructor(videoId) {
|
|
49
49
|
super(`The video with ID "${videoId}" is no longer available or has been removed. Please check the video URL or ID and try again.`);
|
|
50
50
|
this.name = 'YoutubeTranscriptVideoUnavailableError';
|
|
51
|
+
this.videoId = videoId;
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
class YoutubeTranscriptDisabledError extends Error {
|
|
54
55
|
constructor(videoId) {
|
|
55
56
|
super(`Transcripts are disabled for the video with ID "${videoId}". This may be due to the video owner disabling captions or the video not supporting transcripts.`);
|
|
56
57
|
this.name = 'YoutubeTranscriptDisabledError';
|
|
58
|
+
this.videoId = videoId;
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
class YoutubeTranscriptNotAvailableError extends Error {
|
|
60
62
|
constructor(videoId) {
|
|
61
63
|
super(`No transcripts are available for the video with ID "${videoId}". This may be because the video does not have captions or the captions are not accessible.`);
|
|
62
64
|
this.name = 'YoutubeTranscriptNotAvailableError';
|
|
65
|
+
this.videoId = videoId;
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
class YoutubeTranscriptNotAvailableLanguageError extends Error {
|
|
66
69
|
constructor(lang, availableLangs, videoId) {
|
|
67
70
|
super(`No transcripts are available in "${lang}" for the video with ID "${videoId}". Available languages: ${availableLangs.join(', ')}. Please try a different language.`);
|
|
68
71
|
this.name = 'YoutubeTranscriptNotAvailableLanguageError';
|
|
72
|
+
this.videoId = videoId;
|
|
73
|
+
this.lang = lang;
|
|
74
|
+
this.availableLangs = availableLangs;
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
class YoutubeTranscriptInvalidVideoIdError extends Error {
|
|
@@ -75,8 +81,21 @@ class YoutubeTranscriptInvalidVideoIdError extends Error {
|
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
const RE_VIDEO_ID = /^[a-zA-Z0-9_-]{11}$/;
|
|
85
|
+
const XML_ENTITIES = {
|
|
86
|
+
'&': '&',
|
|
87
|
+
'<': '<',
|
|
88
|
+
'>': '>',
|
|
89
|
+
'"': '"',
|
|
90
|
+
''': "'",
|
|
91
|
+
''': "'",
|
|
92
|
+
};
|
|
93
|
+
const RE_XML_ENTITY = /&(?:amp|lt|gt|quot|apos|#39);/g;
|
|
94
|
+
function decodeXmlEntities(text) {
|
|
95
|
+
return text.replace(RE_XML_ENTITY, (match) => { var _a; return (_a = XML_ENTITIES[match]) !== null && _a !== void 0 ? _a : match; });
|
|
96
|
+
}
|
|
78
97
|
function retrieveVideoId(videoId) {
|
|
79
|
-
if (videoId
|
|
98
|
+
if (RE_VIDEO_ID.test(videoId)) {
|
|
80
99
|
return videoId;
|
|
81
100
|
}
|
|
82
101
|
const matchId = videoId.match(RE_YOUTUBE);
|
|
@@ -100,15 +119,19 @@ function defaultFetch(params) {
|
|
|
100
119
|
});
|
|
101
120
|
}
|
|
102
121
|
|
|
122
|
+
function sanitizeKey(key) {
|
|
123
|
+
return key.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
124
|
+
}
|
|
103
125
|
class FsCache {
|
|
104
126
|
constructor(cacheDir = './cache', defaultTTL = DEFAULT_CACHE_TTL) {
|
|
105
127
|
this.cacheDir = cacheDir;
|
|
106
128
|
this.defaultTTL = defaultTTL;
|
|
107
|
-
fs.mkdir(cacheDir, { recursive: true }).
|
|
129
|
+
this.ready = fs.mkdir(cacheDir, { recursive: true }).then(() => { });
|
|
108
130
|
}
|
|
109
131
|
get(key) {
|
|
110
132
|
return __awaiter(this, void 0, void 0, function* () {
|
|
111
|
-
|
|
133
|
+
yield this.ready;
|
|
134
|
+
const filePath = path.join(this.cacheDir, sanitizeKey(key));
|
|
112
135
|
try {
|
|
113
136
|
const data = yield fs.readFile(filePath, 'utf-8');
|
|
114
137
|
const { value, expires } = JSON.parse(data);
|
|
@@ -117,13 +140,14 @@ class FsCache {
|
|
|
117
140
|
}
|
|
118
141
|
yield fs.unlink(filePath);
|
|
119
142
|
}
|
|
120
|
-
catch (
|
|
143
|
+
catch (_error) { }
|
|
121
144
|
return null;
|
|
122
145
|
});
|
|
123
146
|
}
|
|
124
147
|
set(key, value, ttl) {
|
|
125
148
|
return __awaiter(this, void 0, void 0, function* () {
|
|
126
|
-
|
|
149
|
+
yield this.ready;
|
|
150
|
+
const filePath = path.join(this.cacheDir, sanitizeKey(key));
|
|
127
151
|
const expires = Date.now() + (ttl !== null && ttl !== void 0 ? ttl : this.defaultTTL);
|
|
128
152
|
yield fs.writeFile(filePath, JSON.stringify({ value, expires }), 'utf-8');
|
|
129
153
|
});
|
|
@@ -209,7 +233,7 @@ class YoutubeTranscript {
|
|
|
209
233
|
}
|
|
210
234
|
const apiKey = apiKeyMatch[1];
|
|
211
235
|
// 3) Call Innertube player as ANDROID client to retrieve captionTracks
|
|
212
|
-
const playerEndpoint =
|
|
236
|
+
const playerEndpoint = `${protocol}://www.youtube.com/youtubei/v1/player?key=${apiKey}`;
|
|
213
237
|
const playerBody = {
|
|
214
238
|
context: {
|
|
215
239
|
client: {
|
|
@@ -262,7 +286,7 @@ class YoutubeTranscript {
|
|
|
262
286
|
if (!transcriptURL) {
|
|
263
287
|
throw new YoutubeTranscriptNotAvailableError(identifier);
|
|
264
288
|
}
|
|
265
|
-
transcriptURL = transcriptURL.replace(/&fmt=[^&]
|
|
289
|
+
transcriptURL = transcriptURL.replace(/&fmt=[^&]+/, '');
|
|
266
290
|
if ((_m = this.config) === null || _m === void 0 ? void 0 : _m.disableHttps) {
|
|
267
291
|
transcriptURL = transcriptURL.replace(/^https:\/\//, 'http://');
|
|
268
292
|
}
|
|
@@ -281,7 +305,7 @@ class YoutubeTranscript {
|
|
|
281
305
|
// 6) Parse XML into the existing TranscriptResponse shape
|
|
282
306
|
const results = [...transcriptBody.matchAll(RE_XML_TRANSCRIPT)];
|
|
283
307
|
const transcript = results.map((m) => ({
|
|
284
|
-
text: m[3],
|
|
308
|
+
text: decodeXmlEntities(m[3]),
|
|
285
309
|
duration: parseFloat(m[2]),
|
|
286
310
|
offset: parseFloat(m[1]),
|
|
287
311
|
lang: lang !== null && lang !== void 0 ? lang : selectedTrack.languageCode,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "youtube-transcript-plus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Fetch transcript from a YouTube video",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/youtube-transcript-plus.js",
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "rollup -c",
|
|
11
11
|
"format": "prettier --write 'src/**/*.ts'",
|
|
12
|
+
"lint": "eslint .",
|
|
12
13
|
"test": "jest",
|
|
13
14
|
"test:watch": "jest --watch",
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
14
16
|
"prepare": "husky"
|
|
15
17
|
},
|
|
16
18
|
"author": "ericmmartin",
|
|
@@ -29,18 +31,23 @@
|
|
|
29
31
|
]
|
|
30
32
|
},
|
|
31
33
|
"devDependencies": {
|
|
34
|
+
"@eslint/js": "^9.39.2",
|
|
32
35
|
"@types/jest": "^30.0.0",
|
|
36
|
+
"eslint": "^9.39.2",
|
|
37
|
+
"eslint-config-prettier": "^10.1.8",
|
|
38
|
+
"globals": "^16.5.0",
|
|
33
39
|
"https-proxy-agent": "^7.0.6",
|
|
34
40
|
"husky": "^9.1.7",
|
|
35
41
|
"jest": "^30.0.5",
|
|
36
42
|
"lint-staged": "^16.1.5",
|
|
43
|
+
"nock": "^14.0.10",
|
|
37
44
|
"prettier": "^3.6.2",
|
|
38
45
|
"rollup": "^4.46.4",
|
|
39
|
-
"rollup-plugin-typescript": "^1.0.1",
|
|
40
46
|
"rollup-plugin-typescript2": "^0.36.0",
|
|
41
47
|
"ts-jest": "^29.4.1",
|
|
42
48
|
"tslib": "^2.8.1",
|
|
43
|
-
"typescript": "^5.9.2"
|
|
49
|
+
"typescript": "^5.9.2",
|
|
50
|
+
"typescript-eslint": "^8.50.0"
|
|
44
51
|
},
|
|
45
52
|
"files": [
|
|
46
53
|
"dist/*"
|