ytranscript-api 0.1.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 +59 -0
- package/dist/index.cjs +114 -0
- package/dist/index.d.cts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +88 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ytranscript-api
|
|
2
|
+
|
|
3
|
+
Official JavaScript/TypeScript client for the [yTranscript API](https://ytranscript.com/developers) — reliable YouTube transcript extraction without IP bans, proxy management, or player-response scraping.
|
|
4
|
+
|
|
5
|
+
- **No IP bans** — the managed extraction layer absorbs YouTube's bot detection for you
|
|
6
|
+
- **Captionless videos work** — automatic Whisper speech-to-text fallback
|
|
7
|
+
- **Simple, typed** — one method, structured segments, zero dependencies
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install ytranscript-api
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { YTranscript } from "ytranscript-api";
|
|
19
|
+
|
|
20
|
+
const yt = new YTranscript("yk_live_..."); // get a key at ytranscript.com/developers
|
|
21
|
+
|
|
22
|
+
const t = await yt.transcript("dQw4w9WgXcQ"); // video ID or any YouTube URL
|
|
23
|
+
console.log(t.lang); // "en"
|
|
24
|
+
console.log(t.segments[0]); // { text, offset, duration }
|
|
25
|
+
console.log(t.text); // full transcript as one string
|
|
26
|
+
|
|
27
|
+
console.log(yt.lastQuota); // { limit, used, rateLimitRemaining }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Options
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
await yt.transcript("https://youtu.be/dQw4w9WgXcQ", { lang: "es" });
|
|
34
|
+
|
|
35
|
+
new YTranscript(key, { timeoutMs: 60_000 });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Error handling
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { YTranscriptError } from "ytranscript-api";
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await yt.transcript(videoId);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err instanceof YTranscriptError) {
|
|
47
|
+
err.code; // "no_transcript" | "quota_exceeded" | "rate_limited" | ...
|
|
48
|
+
err.status; // HTTP status
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Pricing & keys
|
|
54
|
+
|
|
55
|
+
Plans start at $29/mo for 10,000 units (captions = 1 unit, Whisper = 15 units). Get an API key at **[ytranscript.com/developers](https://ytranscript.com/developers)**.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
YTranscript: () => YTranscript,
|
|
24
|
+
YTranscriptError: () => YTranscriptError,
|
|
25
|
+
default: () => index_default
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var DEFAULT_BASE_URL = "https://ytranscript.com/api/v1";
|
|
29
|
+
var YTranscriptError = class extends Error {
|
|
30
|
+
code;
|
|
31
|
+
status;
|
|
32
|
+
constructor(code, status, message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "YTranscriptError";
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.status = status;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var YTranscript = class {
|
|
40
|
+
apiKey;
|
|
41
|
+
baseUrl;
|
|
42
|
+
timeoutMs;
|
|
43
|
+
/** Quota headers from the most recent successful request */
|
|
44
|
+
lastQuota = null;
|
|
45
|
+
constructor(apiKey, options = {}) {
|
|
46
|
+
if (!apiKey || !apiKey.startsWith("yk_")) {
|
|
47
|
+
throw new YTranscriptError(
|
|
48
|
+
"unauthorized",
|
|
49
|
+
0,
|
|
50
|
+
"An API key (yk_live_...) is required. Get one at https://ytranscript.com/developers"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
this.apiKey = apiKey;
|
|
54
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
55
|
+
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Fetch the transcript for a video. Accepts a video ID or any YouTube URL.
|
|
59
|
+
*/
|
|
60
|
+
async transcript(videoIdOrUrl, options = {}) {
|
|
61
|
+
const params = new URLSearchParams();
|
|
62
|
+
if (/^[\w-]{11}$/.test(videoIdOrUrl)) {
|
|
63
|
+
params.set("videoId", videoIdOrUrl);
|
|
64
|
+
} else {
|
|
65
|
+
params.set("url", videoIdOrUrl);
|
|
66
|
+
}
|
|
67
|
+
if (options.lang) params.set("lang", options.lang);
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await fetch(`${this.baseUrl}/transcript?${params}`, {
|
|
71
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
72
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
throw new YTranscriptError(
|
|
76
|
+
"network_error",
|
|
77
|
+
0,
|
|
78
|
+
`Request failed: ${err instanceof Error ? err.message : "unknown network error"}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const limit = Number(response.headers.get("x-quota-limit"));
|
|
82
|
+
const used = Number(response.headers.get("x-quota-used"));
|
|
83
|
+
const rateRemaining = Number(response.headers.get("x-ratelimit-remaining"));
|
|
84
|
+
if (Number.isFinite(limit) && Number.isFinite(used)) {
|
|
85
|
+
this.lastQuota = {
|
|
86
|
+
limit,
|
|
87
|
+
used,
|
|
88
|
+
rateLimitRemaining: Number.isFinite(rateRemaining) ? rateRemaining : 0
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const body = await response.json().catch(() => null);
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const code = body?.error?.code ?? "fetch_failed";
|
|
94
|
+
const message = body?.error?.message ?? `Request failed with status ${response.status}`;
|
|
95
|
+
throw new YTranscriptError(code, response.status, message);
|
|
96
|
+
}
|
|
97
|
+
const segments = body.segments ?? [];
|
|
98
|
+
return {
|
|
99
|
+
videoId: body.video_id,
|
|
100
|
+
lang: body.lang,
|
|
101
|
+
whisper: Boolean(body.whisper),
|
|
102
|
+
cached: Boolean(body.cached),
|
|
103
|
+
unitsCharged: Number(body.units_charged ?? 1),
|
|
104
|
+
segments,
|
|
105
|
+
text: segments.map((s) => s.text).join(" ")
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var index_default = YTranscript;
|
|
110
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
111
|
+
0 && (module.exports = {
|
|
112
|
+
YTranscript,
|
|
113
|
+
YTranscriptError
|
|
114
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ytranscript-api — official JavaScript/TypeScript client for the yTranscript API.
|
|
3
|
+
*
|
|
4
|
+
* Docs & API keys: https://ytranscript.com/developers
|
|
5
|
+
*/
|
|
6
|
+
interface TranscriptSegment {
|
|
7
|
+
/** Segment text */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Start offset in milliseconds */
|
|
10
|
+
offset: number;
|
|
11
|
+
/** Duration in milliseconds */
|
|
12
|
+
duration: number;
|
|
13
|
+
}
|
|
14
|
+
interface Transcript {
|
|
15
|
+
videoId: string;
|
|
16
|
+
/** BCP-47-ish language code of the returned transcript */
|
|
17
|
+
lang: string;
|
|
18
|
+
/** True when the transcript was produced by Whisper speech-to-text (captionless video) */
|
|
19
|
+
whisper: boolean;
|
|
20
|
+
/** True when served from cache */
|
|
21
|
+
cached: boolean;
|
|
22
|
+
/** Quota units this request consumed (captions: 1, Whisper: 15) */
|
|
23
|
+
unitsCharged: number;
|
|
24
|
+
segments: TranscriptSegment[];
|
|
25
|
+
/** Convenience: all segment text joined with spaces */
|
|
26
|
+
text: string;
|
|
27
|
+
}
|
|
28
|
+
interface QuotaInfo {
|
|
29
|
+
limit: number;
|
|
30
|
+
used: number;
|
|
31
|
+
rateLimitRemaining: number;
|
|
32
|
+
}
|
|
33
|
+
type ErrorCode = "unauthorized" | "quota_exceeded" | "rate_limited" | "bad_request" | "no_transcript" | "fetch_failed" | "network_error";
|
|
34
|
+
declare class YTranscriptError extends Error {
|
|
35
|
+
readonly code: ErrorCode;
|
|
36
|
+
readonly status: number;
|
|
37
|
+
constructor(code: ErrorCode, status: number, message: string);
|
|
38
|
+
}
|
|
39
|
+
interface ClientOptions {
|
|
40
|
+
/** Override the API base URL (rarely needed) */
|
|
41
|
+
baseUrl?: string;
|
|
42
|
+
/** Request timeout in milliseconds (default 120s — Whisper fallbacks take time) */
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}
|
|
45
|
+
interface TranscriptOptions {
|
|
46
|
+
/** Preferred language code (e.g. "en", "es"). Defaults to the video's source language. */
|
|
47
|
+
lang?: string;
|
|
48
|
+
}
|
|
49
|
+
declare class YTranscript {
|
|
50
|
+
private readonly apiKey;
|
|
51
|
+
private readonly baseUrl;
|
|
52
|
+
private readonly timeoutMs;
|
|
53
|
+
/** Quota headers from the most recent successful request */
|
|
54
|
+
lastQuota: QuotaInfo | null;
|
|
55
|
+
constructor(apiKey: string, options?: ClientOptions);
|
|
56
|
+
/**
|
|
57
|
+
* Fetch the transcript for a video. Accepts a video ID or any YouTube URL.
|
|
58
|
+
*/
|
|
59
|
+
transcript(videoIdOrUrl: string, options?: TranscriptOptions): Promise<Transcript>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { type ClientOptions, type ErrorCode, type QuotaInfo, type Transcript, type TranscriptOptions, type TranscriptSegment, YTranscript, YTranscriptError, YTranscript as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ytranscript-api — official JavaScript/TypeScript client for the yTranscript API.
|
|
3
|
+
*
|
|
4
|
+
* Docs & API keys: https://ytranscript.com/developers
|
|
5
|
+
*/
|
|
6
|
+
interface TranscriptSegment {
|
|
7
|
+
/** Segment text */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Start offset in milliseconds */
|
|
10
|
+
offset: number;
|
|
11
|
+
/** Duration in milliseconds */
|
|
12
|
+
duration: number;
|
|
13
|
+
}
|
|
14
|
+
interface Transcript {
|
|
15
|
+
videoId: string;
|
|
16
|
+
/** BCP-47-ish language code of the returned transcript */
|
|
17
|
+
lang: string;
|
|
18
|
+
/** True when the transcript was produced by Whisper speech-to-text (captionless video) */
|
|
19
|
+
whisper: boolean;
|
|
20
|
+
/** True when served from cache */
|
|
21
|
+
cached: boolean;
|
|
22
|
+
/** Quota units this request consumed (captions: 1, Whisper: 15) */
|
|
23
|
+
unitsCharged: number;
|
|
24
|
+
segments: TranscriptSegment[];
|
|
25
|
+
/** Convenience: all segment text joined with spaces */
|
|
26
|
+
text: string;
|
|
27
|
+
}
|
|
28
|
+
interface QuotaInfo {
|
|
29
|
+
limit: number;
|
|
30
|
+
used: number;
|
|
31
|
+
rateLimitRemaining: number;
|
|
32
|
+
}
|
|
33
|
+
type ErrorCode = "unauthorized" | "quota_exceeded" | "rate_limited" | "bad_request" | "no_transcript" | "fetch_failed" | "network_error";
|
|
34
|
+
declare class YTranscriptError extends Error {
|
|
35
|
+
readonly code: ErrorCode;
|
|
36
|
+
readonly status: number;
|
|
37
|
+
constructor(code: ErrorCode, status: number, message: string);
|
|
38
|
+
}
|
|
39
|
+
interface ClientOptions {
|
|
40
|
+
/** Override the API base URL (rarely needed) */
|
|
41
|
+
baseUrl?: string;
|
|
42
|
+
/** Request timeout in milliseconds (default 120s — Whisper fallbacks take time) */
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}
|
|
45
|
+
interface TranscriptOptions {
|
|
46
|
+
/** Preferred language code (e.g. "en", "es"). Defaults to the video's source language. */
|
|
47
|
+
lang?: string;
|
|
48
|
+
}
|
|
49
|
+
declare class YTranscript {
|
|
50
|
+
private readonly apiKey;
|
|
51
|
+
private readonly baseUrl;
|
|
52
|
+
private readonly timeoutMs;
|
|
53
|
+
/** Quota headers from the most recent successful request */
|
|
54
|
+
lastQuota: QuotaInfo | null;
|
|
55
|
+
constructor(apiKey: string, options?: ClientOptions);
|
|
56
|
+
/**
|
|
57
|
+
* Fetch the transcript for a video. Accepts a video ID or any YouTube URL.
|
|
58
|
+
*/
|
|
59
|
+
transcript(videoIdOrUrl: string, options?: TranscriptOptions): Promise<Transcript>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { type ClientOptions, type ErrorCode, type QuotaInfo, type Transcript, type TranscriptOptions, type TranscriptSegment, YTranscript, YTranscriptError, YTranscript as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://ytranscript.com/api/v1";
|
|
3
|
+
var YTranscriptError = class extends Error {
|
|
4
|
+
code;
|
|
5
|
+
status;
|
|
6
|
+
constructor(code, status, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "YTranscriptError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.status = status;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var YTranscript = class {
|
|
14
|
+
apiKey;
|
|
15
|
+
baseUrl;
|
|
16
|
+
timeoutMs;
|
|
17
|
+
/** Quota headers from the most recent successful request */
|
|
18
|
+
lastQuota = null;
|
|
19
|
+
constructor(apiKey, options = {}) {
|
|
20
|
+
if (!apiKey || !apiKey.startsWith("yk_")) {
|
|
21
|
+
throw new YTranscriptError(
|
|
22
|
+
"unauthorized",
|
|
23
|
+
0,
|
|
24
|
+
"An API key (yk_live_...) is required. Get one at https://ytranscript.com/developers"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
this.apiKey = apiKey;
|
|
28
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
29
|
+
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fetch the transcript for a video. Accepts a video ID or any YouTube URL.
|
|
33
|
+
*/
|
|
34
|
+
async transcript(videoIdOrUrl, options = {}) {
|
|
35
|
+
const params = new URLSearchParams();
|
|
36
|
+
if (/^[\w-]{11}$/.test(videoIdOrUrl)) {
|
|
37
|
+
params.set("videoId", videoIdOrUrl);
|
|
38
|
+
} else {
|
|
39
|
+
params.set("url", videoIdOrUrl);
|
|
40
|
+
}
|
|
41
|
+
if (options.lang) params.set("lang", options.lang);
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await fetch(`${this.baseUrl}/transcript?${params}`, {
|
|
45
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
46
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
47
|
+
});
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw new YTranscriptError(
|
|
50
|
+
"network_error",
|
|
51
|
+
0,
|
|
52
|
+
`Request failed: ${err instanceof Error ? err.message : "unknown network error"}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const limit = Number(response.headers.get("x-quota-limit"));
|
|
56
|
+
const used = Number(response.headers.get("x-quota-used"));
|
|
57
|
+
const rateRemaining = Number(response.headers.get("x-ratelimit-remaining"));
|
|
58
|
+
if (Number.isFinite(limit) && Number.isFinite(used)) {
|
|
59
|
+
this.lastQuota = {
|
|
60
|
+
limit,
|
|
61
|
+
used,
|
|
62
|
+
rateLimitRemaining: Number.isFinite(rateRemaining) ? rateRemaining : 0
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const body = await response.json().catch(() => null);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const code = body?.error?.code ?? "fetch_failed";
|
|
68
|
+
const message = body?.error?.message ?? `Request failed with status ${response.status}`;
|
|
69
|
+
throw new YTranscriptError(code, response.status, message);
|
|
70
|
+
}
|
|
71
|
+
const segments = body.segments ?? [];
|
|
72
|
+
return {
|
|
73
|
+
videoId: body.video_id,
|
|
74
|
+
lang: body.lang,
|
|
75
|
+
whisper: Boolean(body.whisper),
|
|
76
|
+
cached: Boolean(body.cached),
|
|
77
|
+
unitsCharged: Number(body.units_charged ?? 1),
|
|
78
|
+
segments,
|
|
79
|
+
text: segments.map((s) => s.text).join(" ")
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var index_default = YTranscript;
|
|
84
|
+
export {
|
|
85
|
+
YTranscript,
|
|
86
|
+
YTranscriptError,
|
|
87
|
+
index_default as default
|
|
88
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ytranscript-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reliable YouTube transcript extraction API client — no IP bans, no proxy management, Whisper fallback for captionless videos.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"youtube",
|
|
7
|
+
"transcript",
|
|
8
|
+
"youtube transcript api",
|
|
9
|
+
"captions",
|
|
10
|
+
"subtitles",
|
|
11
|
+
"whisper",
|
|
12
|
+
"speech-to-text"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://ytranscript.com/developers",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/nnnoel/ytranscript.git",
|
|
18
|
+
"directory": "sdk/js"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "yTranscript <support@ytranscript.com>",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"require": "./dist/index.cjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
39
|
+
"prepublishOnly": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"tsup": "^8.0.0",
|
|
43
|
+
"typescript": "^5.4.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
}
|
|
48
|
+
}
|