@thenavidm/youtube-mcp-cli 2.0.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/LICENSE +21 -0
- package/README.md +877 -0
- package/SKILL.md +162 -0
- package/dist/accounts/store.d.ts +41 -0
- package/dist/accounts/store.js +110 -0
- package/dist/accounts/store.js.map +1 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +206 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +439 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +130 -0
- package/dist/config.js.map +1 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +78 -0
- package/dist/doctor.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +134 -0
- package/dist/index.js.map +1 -0
- package/dist/safety.d.ts +50 -0
- package/dist/safety.js +84 -0
- package/dist/safety.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +28 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/account.d.ts +10 -0
- package/dist/tools/account.js +222 -0
- package/dist/tools/account.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/kit.d.ts +57 -0
- package/dist/tools/kit.js +78 -0
- package/dist/tools/kit.js.map +1 -0
- package/dist/tools/research.d.ts +16 -0
- package/dist/tools/research.js +232 -0
- package/dist/tools/research.js.map +1 -0
- package/dist/tools/transcripts.d.ts +9 -0
- package/dist/tools/transcripts.js +123 -0
- package/dist/tools/transcripts.js.map +1 -0
- package/dist/tools/types.d.ts +6 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/transport/http.d.ts +23 -0
- package/dist/transport/http.js +83 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/youtube/api.d.ts +47 -0
- package/dist/youtube/api.js +176 -0
- package/dist/youtube/api.js.map +1 -0
- package/dist/youtube/transcripts.d.ts +65 -0
- package/dist/youtube/transcripts.js +321 -0
- package/dist/youtube/transcripts.js.map +1 -0
- package/dist/youtube/ytdlp.d.ts +35 -0
- package/dist/youtube/ytdlp.js +145 -0
- package/dist/youtube/ytdlp.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouTube Data + Analytics API client.
|
|
3
|
+
*
|
|
4
|
+
* Two credential paths, and which one you have decides which tools work:
|
|
5
|
+
*
|
|
6
|
+
* API key: anyone's public data. Search, channels, public video stats.
|
|
7
|
+
* No consent screen, no user. Cheapest to set up.
|
|
8
|
+
* OAuth: your own channels. Uploads, playlists, comments as the owner,
|
|
9
|
+
* and Analytics, which has no API-key path at all.
|
|
10
|
+
*
|
|
11
|
+
* Multi-account is the default rather than a feature. A creator with several
|
|
12
|
+
* channels has to be able to say which one an action runs against, so every
|
|
13
|
+
* account-scoped tool takes `account` and the server refuses to guess when
|
|
14
|
+
* more than one is connected.
|
|
15
|
+
*/
|
|
16
|
+
const DATA_BASE = "https://www.googleapis.com/youtube/v3";
|
|
17
|
+
const ANALYTICS_BASE = "https://youtubeanalytics.googleapis.com/v2";
|
|
18
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
19
|
+
export class YouTubeApiError extends Error {
|
|
20
|
+
status;
|
|
21
|
+
reason;
|
|
22
|
+
constructor(message, status, reason) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.reason = reason;
|
|
26
|
+
this.name = "YouTubeApiError";
|
|
27
|
+
}
|
|
28
|
+
/** The shape the CLI prints on stderr, so a script parses one thing. */
|
|
29
|
+
toJSON() {
|
|
30
|
+
return { error: this.message, status: this.status, ...(this.reason ? { reason: this.reason } : {}) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Turn Google's error envelope into something a person can act on.
|
|
35
|
+
*
|
|
36
|
+
* The raw messages are unhelpfully generic: "The request cannot be completed
|
|
37
|
+
* because you have exceeded your quota" does not tell you that quota resets at
|
|
38
|
+
* midnight Pacific, and `quotaExceeded` vs `rateLimitExceeded` need different
|
|
39
|
+
* responses from the caller.
|
|
40
|
+
*/
|
|
41
|
+
function explain(status, reason, message) {
|
|
42
|
+
switch (reason) {
|
|
43
|
+
case "quotaExceeded":
|
|
44
|
+
return "Daily API quota is used up. It resets at midnight Pacific. Raise it in the Cloud console under IAM & Admin > Quotas, or wait.";
|
|
45
|
+
case "rateLimitExceeded":
|
|
46
|
+
return "Too many requests too quickly. Slow down and retry.";
|
|
47
|
+
case "forbidden":
|
|
48
|
+
case "insufficientPermissions":
|
|
49
|
+
return `Your token lacks the scope for this call. Reconnect the account and approve every permission. (${message})`;
|
|
50
|
+
case "authError":
|
|
51
|
+
case "unauthorized":
|
|
52
|
+
return "The access token is invalid or expired. Reconnect the account.";
|
|
53
|
+
case "videoNotFound":
|
|
54
|
+
case "channelNotFound":
|
|
55
|
+
case "playlistNotFound":
|
|
56
|
+
return `Not found. Check the id. (${message})`;
|
|
57
|
+
default:
|
|
58
|
+
return status === 403
|
|
59
|
+
? `${message}. Usually a missing scope or a disabled API in the Cloud project.`
|
|
60
|
+
: message;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export class YouTubeClient {
|
|
64
|
+
creds;
|
|
65
|
+
/** Refreshed tokens live here for the process lifetime, keyed by account id. */
|
|
66
|
+
tokenCache = new Map();
|
|
67
|
+
constructor(creds) {
|
|
68
|
+
this.creds = creds;
|
|
69
|
+
}
|
|
70
|
+
/** An access token for the account, refreshing when it is within a minute of expiry. */
|
|
71
|
+
async accessToken(account) {
|
|
72
|
+
const cached = this.tokenCache.get(account.id);
|
|
73
|
+
if (cached && cached.expiresAt > Date.now() + 60_000)
|
|
74
|
+
return cached.token;
|
|
75
|
+
if (!account.refreshToken) {
|
|
76
|
+
if (!account.accessToken) {
|
|
77
|
+
throw new YouTubeApiError(`Account "${account.name}" has no credentials. Reconnect it.`, 401, "authError");
|
|
78
|
+
}
|
|
79
|
+
return account.accessToken;
|
|
80
|
+
}
|
|
81
|
+
const res = await fetch(TOKEN_URL, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
84
|
+
body: new URLSearchParams({
|
|
85
|
+
grant_type: "refresh_token",
|
|
86
|
+
refresh_token: account.refreshToken,
|
|
87
|
+
client_id: account.clientId,
|
|
88
|
+
client_secret: account.clientSecret,
|
|
89
|
+
}).toString(),
|
|
90
|
+
});
|
|
91
|
+
const body = (await res.json());
|
|
92
|
+
if (!res.ok || !body.access_token) {
|
|
93
|
+
// `unauthorized_client` here nearly always means the refresh token was
|
|
94
|
+
// issued by a different OAuth client than the one configured now, not
|
|
95
|
+
// that the user revoked access. Say so, because the two look identical
|
|
96
|
+
// from the outside and lead to opposite fixes.
|
|
97
|
+
const hint = body.error === "unauthorized_client"
|
|
98
|
+
? " This usually means the token was issued by a different OAuth client than the one configured. Check YOUTUBE_CLIENT_ID matches the client that authorized this channel, or reconnect."
|
|
99
|
+
: "";
|
|
100
|
+
throw new YouTubeApiError(`Could not refresh "${account.name}": ${body.error ?? res.status}${body.error_description ? ` (${body.error_description})` : ""}.${hint}`, 401, "authError");
|
|
101
|
+
}
|
|
102
|
+
this.tokenCache.set(account.id, {
|
|
103
|
+
token: body.access_token,
|
|
104
|
+
expiresAt: Date.now() + (body.expires_in ?? 3600) * 1000,
|
|
105
|
+
});
|
|
106
|
+
return body.access_token;
|
|
107
|
+
}
|
|
108
|
+
async request(base, path, params, init = {}) {
|
|
109
|
+
const url = new URL(`${base}${path}`);
|
|
110
|
+
for (const [k, v] of Object.entries(params)) {
|
|
111
|
+
if (v === undefined || v === null || v === "")
|
|
112
|
+
continue;
|
|
113
|
+
url.searchParams.set(k, String(v));
|
|
114
|
+
}
|
|
115
|
+
const headers = {};
|
|
116
|
+
const account = this.creds.account;
|
|
117
|
+
if (account) {
|
|
118
|
+
headers.Authorization = `Bearer ${await this.accessToken(account)}`;
|
|
119
|
+
}
|
|
120
|
+
else if (init.requireAuth) {
|
|
121
|
+
// A plain Error, not a 401: nothing was rejected, nothing was set up. The
|
|
122
|
+
// CLI maps "no ... is configured" to exit 10, which is what a script needs
|
|
123
|
+
// to tell "run login first" apart from "your token expired".
|
|
124
|
+
throw new Error("No account is configured for this action. Run `youtube-cli login` once per channel, or set YOUTUBE_ACCOUNTS.");
|
|
125
|
+
}
|
|
126
|
+
else if (this.creds.apiKey) {
|
|
127
|
+
url.searchParams.set("key", this.creds.apiKey);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
throw new Error("No API key is configured. Run `youtube-cli login --api-key KEY` or set YOUTUBE_API_KEY for public data, or `youtube-cli login` for your own channels.");
|
|
131
|
+
}
|
|
132
|
+
if (init.body !== undefined)
|
|
133
|
+
headers["Content-Type"] = "application/json";
|
|
134
|
+
const res = await fetch(url, {
|
|
135
|
+
method: init.method ?? "GET",
|
|
136
|
+
headers,
|
|
137
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
138
|
+
});
|
|
139
|
+
const text = await res.text();
|
|
140
|
+
const data = text ? JSON.parse(text) : {};
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
const err = data.error;
|
|
143
|
+
const reason = err?.errors?.[0]?.reason;
|
|
144
|
+
throw new YouTubeApiError(explain(res.status, reason, err?.message ?? text), res.status, reason);
|
|
145
|
+
}
|
|
146
|
+
return data;
|
|
147
|
+
}
|
|
148
|
+
get(path, params = {}, requireAuth = false) {
|
|
149
|
+
return this.request(DATA_BASE, path, params, { requireAuth });
|
|
150
|
+
}
|
|
151
|
+
post(path, params, body) {
|
|
152
|
+
return this.request(DATA_BASE, path, params, { method: "POST", body, requireAuth: true });
|
|
153
|
+
}
|
|
154
|
+
put(path, params, body) {
|
|
155
|
+
return this.request(DATA_BASE, path, params, { method: "PUT", body, requireAuth: true });
|
|
156
|
+
}
|
|
157
|
+
delete(path, params) {
|
|
158
|
+
return this.request(DATA_BASE, path, params, { method: "DELETE", requireAuth: true });
|
|
159
|
+
}
|
|
160
|
+
analytics(params) {
|
|
161
|
+
return this.request(ANALYTICS_BASE, "/reports", params, { requireAuth: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** ISO 8601 durations (`PT4M13S`) are unreadable in output. Seconds are not. */
|
|
165
|
+
export function durationToSeconds(iso) {
|
|
166
|
+
if (!iso)
|
|
167
|
+
return null;
|
|
168
|
+
const m = /^P(?:(\d+)D)?T?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/.exec(iso);
|
|
169
|
+
if (!m)
|
|
170
|
+
return null;
|
|
171
|
+
return (Number(m[1] ?? 0) * 86400 +
|
|
172
|
+
Number(m[2] ?? 0) * 3600 +
|
|
173
|
+
Number(m[3] ?? 0) * 60 +
|
|
174
|
+
Number(m[4] ?? 0));
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/youtube/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcripts.
|
|
3
|
+
*
|
|
4
|
+
* These need no API key, no OAuth, and no quota. YouTube serves caption tracks
|
|
5
|
+
* to the watch page itself, and that is what we read. It is the only part of
|
|
6
|
+
* this server that works the moment it is installed.
|
|
7
|
+
*
|
|
8
|
+
* The trade is that it is not a supported API. YouTube changes the watch page
|
|
9
|
+
* shape occasionally and this has to follow. Everything here is written to fail
|
|
10
|
+
* as "no transcript" rather than to throw, because a missing transcript is a
|
|
11
|
+
* normal outcome: plenty of videos genuinely have no captions.
|
|
12
|
+
*
|
|
13
|
+
* Do not confuse this with the Captions API (`captions.download`), which the
|
|
14
|
+
* account tools use. That one needs OAuth AND channel ownership, so it can only
|
|
15
|
+
* ever read your own videos. This reads anyone's.
|
|
16
|
+
*/
|
|
17
|
+
export type TranscriptSegment = {
|
|
18
|
+
/** Seconds from the start of the video. */
|
|
19
|
+
start: number;
|
|
20
|
+
/** Segment duration in seconds. */
|
|
21
|
+
duration: number;
|
|
22
|
+
text: string;
|
|
23
|
+
};
|
|
24
|
+
export type TranscriptTrack = {
|
|
25
|
+
languageCode: string;
|
|
26
|
+
languageName: string;
|
|
27
|
+
/** True when YouTube generated these captions rather than a human writing them. */
|
|
28
|
+
autoGenerated: boolean;
|
|
29
|
+
url: string;
|
|
30
|
+
};
|
|
31
|
+
export declare class TranscriptError extends Error {
|
|
32
|
+
readonly code: "no_video" | "no_captions" | "language_missing" | "blocked" | "network";
|
|
33
|
+
constructor(message: string, code: "no_video" | "no_captions" | "language_missing" | "blocked" | "network");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Accepts anything a user is likely to paste: a bare id, a watch URL, a
|
|
37
|
+
* youtu.be link, a Shorts URL, an embed, or a live URL.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseVideoId(input: string): string | null;
|
|
40
|
+
/** Every caption track YouTube will serve for this video. */
|
|
41
|
+
export declare function listTracks(videoId: string, timeoutMs?: number): Promise<TranscriptTrack[]>;
|
|
42
|
+
export type FetchTranscriptOptions = {
|
|
43
|
+
language?: string;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
};
|
|
46
|
+
export type Transcript = {
|
|
47
|
+
videoId: string;
|
|
48
|
+
language: string;
|
|
49
|
+
autoGenerated: boolean;
|
|
50
|
+
segments: TranscriptSegment[];
|
|
51
|
+
};
|
|
52
|
+
/** Fetch one video's transcript. Throws TranscriptError with a `code` you can branch on. */
|
|
53
|
+
export declare function fetchTranscript(videoIdOrUrl: string, opts?: FetchTranscriptOptions): Promise<Transcript>;
|
|
54
|
+
/** `1h02m03s` style stamps read better than raw seconds in a transcript dump. */
|
|
55
|
+
export declare function formatTimestamp(seconds: number): string;
|
|
56
|
+
/** Collapse segments into readable prose. Auto-captions arrive as ~2 second fragments. */
|
|
57
|
+
export declare function toPlainText(segments: TranscriptSegment[]): string;
|
|
58
|
+
/** Timestamped lines, grouped so the output is scannable rather than one line per fragment. */
|
|
59
|
+
export declare function toTimestampedText(segments: TranscriptSegment[], groupSeconds?: number): string;
|
|
60
|
+
/** Find a phrase in a transcript and return the timestamps where it occurs. */
|
|
61
|
+
export declare function searchSegments(segments: TranscriptSegment[], query: string, contextSegments?: number): {
|
|
62
|
+
timestamp: string;
|
|
63
|
+
start: number;
|
|
64
|
+
text: string;
|
|
65
|
+
}[];
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcripts.
|
|
3
|
+
*
|
|
4
|
+
* These need no API key, no OAuth, and no quota. YouTube serves caption tracks
|
|
5
|
+
* to the watch page itself, and that is what we read. It is the only part of
|
|
6
|
+
* this server that works the moment it is installed.
|
|
7
|
+
*
|
|
8
|
+
* The trade is that it is not a supported API. YouTube changes the watch page
|
|
9
|
+
* shape occasionally and this has to follow. Everything here is written to fail
|
|
10
|
+
* as "no transcript" rather than to throw, because a missing transcript is a
|
|
11
|
+
* normal outcome: plenty of videos genuinely have no captions.
|
|
12
|
+
*
|
|
13
|
+
* Do not confuse this with the Captions API (`captions.download`), which the
|
|
14
|
+
* account tools use. That one needs OAuth AND channel ownership, so it can only
|
|
15
|
+
* ever read your own videos. This reads anyone's.
|
|
16
|
+
*/
|
|
17
|
+
import { fetchCaptions, YtDlpMissingError } from "./ytdlp.js";
|
|
18
|
+
const WATCH_URL = "https://www.youtube.com/watch";
|
|
19
|
+
// Without a browser-ish UA, YouTube serves a consent interstitial with no
|
|
20
|
+
// player response in it, and every video looks like it has no captions.
|
|
21
|
+
const HEADERS = {
|
|
22
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
23
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
24
|
+
};
|
|
25
|
+
export class TranscriptError extends Error {
|
|
26
|
+
code;
|
|
27
|
+
constructor(message, code) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.name = "TranscriptError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Accepts anything a user is likely to paste: a bare id, a watch URL, a
|
|
35
|
+
* youtu.be link, a Shorts URL, an embed, or a live URL.
|
|
36
|
+
*/
|
|
37
|
+
export function parseVideoId(input) {
|
|
38
|
+
const raw = (input || "").trim();
|
|
39
|
+
if (!raw)
|
|
40
|
+
return null;
|
|
41
|
+
// A bare id. YouTube ids are 11 chars of [A-Za-z0-9_-].
|
|
42
|
+
if (/^[\w-]{11}$/.test(raw))
|
|
43
|
+
return raw;
|
|
44
|
+
let url;
|
|
45
|
+
try {
|
|
46
|
+
url = new URL(raw.startsWith("http") ? raw : `https://${raw}`);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (url.hostname.endsWith("youtu.be")) {
|
|
52
|
+
const id = url.pathname.slice(1).split("/")[0];
|
|
53
|
+
return id && /^[\w-]{11}$/.test(id) ? id : null;
|
|
54
|
+
}
|
|
55
|
+
const v = url.searchParams.get("v");
|
|
56
|
+
if (v && /^[\w-]{11}$/.test(v))
|
|
57
|
+
return v;
|
|
58
|
+
const m = url.pathname.match(/\/(shorts|embed|live|v)\/([\w-]{11})/);
|
|
59
|
+
return m?.[2] ?? null;
|
|
60
|
+
}
|
|
61
|
+
/** Decode the XML entities YouTube puts in caption text, including the double-escaped ones. */
|
|
62
|
+
function decodeEntities(text) {
|
|
63
|
+
return text
|
|
64
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
|
|
65
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
|
|
66
|
+
.replace(/&quot;/g, '"')
|
|
67
|
+
.replace(/"/g, '"')
|
|
68
|
+
.replace(/&#39;/g, "'")
|
|
69
|
+
.replace(/'/g, "'")
|
|
70
|
+
.replace(/&lt;/g, "<")
|
|
71
|
+
.replace(/</g, "<")
|
|
72
|
+
.replace(/&gt;/g, ">")
|
|
73
|
+
.replace(/>/g, ">")
|
|
74
|
+
.replace(/&nbsp;/g, " ")
|
|
75
|
+
.replace(/ /g, " ")
|
|
76
|
+
.replace(/&/g, "&")
|
|
77
|
+
.trim();
|
|
78
|
+
}
|
|
79
|
+
async function fetchText(url, timeoutMs) {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(url, { headers: HEADERS, signal: controller.signal });
|
|
84
|
+
if (res.status === 429) {
|
|
85
|
+
throw new TranscriptError("YouTube is rate limiting transcript requests from this IP. Wait a few minutes, or set YOUTUBE_TRANSCRIPT_PROXY.", "blocked");
|
|
86
|
+
}
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new TranscriptError(`YouTube returned ${res.status} for ${url}`, "network");
|
|
89
|
+
}
|
|
90
|
+
return await res.text();
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err instanceof TranscriptError)
|
|
94
|
+
throw err;
|
|
95
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
96
|
+
throw new TranscriptError(`Timed out after ${timeoutMs}ms fetching the transcript.`, "network");
|
|
97
|
+
}
|
|
98
|
+
throw new TranscriptError(err instanceof Error ? err.message : String(err), "network");
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Pull the caption track list out of the watch page.
|
|
106
|
+
*
|
|
107
|
+
* The player response is embedded in the HTML as a JSON blob. We locate it by
|
|
108
|
+
* its key and balance braces rather than regex the whole object, because the
|
|
109
|
+
* blob contains nested braces inside strings and a greedy regex reliably
|
|
110
|
+
* grabs either too little or the rest of the document.
|
|
111
|
+
*/
|
|
112
|
+
function extractPlayerResponse(html) {
|
|
113
|
+
const key = "ytInitialPlayerResponse";
|
|
114
|
+
const at = html.indexOf(key);
|
|
115
|
+
if (at === -1)
|
|
116
|
+
return null;
|
|
117
|
+
const braceStart = html.indexOf("{", at);
|
|
118
|
+
if (braceStart === -1)
|
|
119
|
+
return null;
|
|
120
|
+
let depth = 0;
|
|
121
|
+
let inString = false;
|
|
122
|
+
let escaped = false;
|
|
123
|
+
for (let i = braceStart; i < html.length; i++) {
|
|
124
|
+
const ch = html[i];
|
|
125
|
+
if (escaped) {
|
|
126
|
+
escaped = false;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (ch === "\\") {
|
|
130
|
+
escaped = true;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === '"') {
|
|
134
|
+
inString = !inString;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (inString)
|
|
138
|
+
continue;
|
|
139
|
+
if (ch === "{")
|
|
140
|
+
depth++;
|
|
141
|
+
else if (ch === "}") {
|
|
142
|
+
depth--;
|
|
143
|
+
if (depth === 0) {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(html.slice(braceStart, i + 1));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
/** Every caption track YouTube will serve for this video. */
|
|
156
|
+
export async function listTracks(videoId, timeoutMs = 15000) {
|
|
157
|
+
const html = await fetchText(`${WATCH_URL}?v=${videoId}&hl=en`, timeoutMs);
|
|
158
|
+
const player = extractPlayerResponse(html);
|
|
159
|
+
if (!player) {
|
|
160
|
+
throw new TranscriptError("Could not read the video page. The video may be private, deleted, or age-restricted.", "no_video");
|
|
161
|
+
}
|
|
162
|
+
const status = player.playabilityStatus?.status;
|
|
163
|
+
if (status && status !== "OK") {
|
|
164
|
+
throw new TranscriptError(`YouTube will not play this video (${status}). Private, deleted, or region-locked videos have no readable transcript.`, "no_video");
|
|
165
|
+
}
|
|
166
|
+
const captions = player.captions;
|
|
167
|
+
const tracks = captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
|
|
168
|
+
return tracks
|
|
169
|
+
.map((t) => ({
|
|
170
|
+
languageCode: String(t.languageCode ?? ""),
|
|
171
|
+
languageName: String(t.name?.simpleText ??
|
|
172
|
+
t.name?.runs?.[0]?.text ??
|
|
173
|
+
t.languageCode ??
|
|
174
|
+
""),
|
|
175
|
+
// `asr` = automatic speech recognition, i.e. YouTube's own auto-captions.
|
|
176
|
+
autoGenerated: String(t.kind ?? "") === "asr",
|
|
177
|
+
url: String(t.baseUrl ?? ""),
|
|
178
|
+
}))
|
|
179
|
+
.filter((t) => t.url);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Pick a track for the requested language.
|
|
183
|
+
*
|
|
184
|
+
* Preference order, and the reason for it: an exact match first; then the same
|
|
185
|
+
* language ignoring region, so `en` finds `en-GB`; then a human-written track
|
|
186
|
+
* in any language, which beats auto-captions on accuracy; then whatever exists.
|
|
187
|
+
*/
|
|
188
|
+
function pickTrack(tracks, language) {
|
|
189
|
+
if (tracks.length === 0)
|
|
190
|
+
return null;
|
|
191
|
+
if (!language) {
|
|
192
|
+
return tracks.find((t) => !t.autoGenerated) ?? tracks[0] ?? null;
|
|
193
|
+
}
|
|
194
|
+
const want = language.toLowerCase();
|
|
195
|
+
return (tracks.find((t) => t.languageCode.toLowerCase() === want) ??
|
|
196
|
+
tracks.find((t) => t.languageCode.toLowerCase().split("-")[0] === want.split("-")[0]) ??
|
|
197
|
+
null);
|
|
198
|
+
}
|
|
199
|
+
function parseTimedText(xml) {
|
|
200
|
+
const out = [];
|
|
201
|
+
const re = /<text([^>]*)>([\s\S]*?)<\/text>/g;
|
|
202
|
+
let m;
|
|
203
|
+
while ((m = re.exec(xml)) !== null) {
|
|
204
|
+
const attrs = m[1] ?? "";
|
|
205
|
+
const body = m[2] ?? "";
|
|
206
|
+
const start = Number(/\bstart="([\d.]+)"/.exec(attrs)?.[1] ?? 0);
|
|
207
|
+
const duration = Number(/\bdur="([\d.]+)"/.exec(attrs)?.[1] ?? 0);
|
|
208
|
+
const text = decodeEntities(body.replace(/<[^>]+>/g, ""));
|
|
209
|
+
if (text)
|
|
210
|
+
out.push({ start, duration, text });
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
/** Fetch one video's transcript. Throws TranscriptError with a `code` you can branch on. */
|
|
215
|
+
export async function fetchTranscript(videoIdOrUrl, opts = {}) {
|
|
216
|
+
const videoId = parseVideoId(videoIdOrUrl);
|
|
217
|
+
if (!videoId) {
|
|
218
|
+
throw new TranscriptError(`Could not read a video id out of "${videoIdOrUrl}". Pass a video id or a YouTube URL.`, "no_video");
|
|
219
|
+
}
|
|
220
|
+
const timeoutMs = opts.timeoutMs ?? 15000;
|
|
221
|
+
const tracks = await listTracks(videoId, timeoutMs);
|
|
222
|
+
if (tracks.length === 0) {
|
|
223
|
+
throw new TranscriptError("This video has no captions at all, so there is no transcript to read.", "no_captions");
|
|
224
|
+
}
|
|
225
|
+
const track = pickTrack(tracks, opts.language);
|
|
226
|
+
if (!track) {
|
|
227
|
+
const available = tracks.map((t) => t.languageCode).join(", ");
|
|
228
|
+
throw new TranscriptError(`No "${opts.language}" captions on this video. Available: ${available}.`, "language_missing");
|
|
229
|
+
}
|
|
230
|
+
// The track list above is real, but YouTube answers the track URL itself with
|
|
231
|
+
// an empty body unless the request carries a proof-of-origin token. yt-dlp
|
|
232
|
+
// mints one, so the body comes from there while the list comes from the page.
|
|
233
|
+
let segments;
|
|
234
|
+
let language = track.languageCode;
|
|
235
|
+
let autoGenerated = track.autoGenerated;
|
|
236
|
+
try {
|
|
237
|
+
const viaYtDlp = await fetchCaptions(videoId, track.languageCode, Math.max(timeoutMs, 90000));
|
|
238
|
+
segments = viaYtDlp.segments;
|
|
239
|
+
language = viaYtDlp.language;
|
|
240
|
+
autoGenerated = viaYtDlp.autoGenerated;
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
if (err instanceof YtDlpMissingError) {
|
|
244
|
+
throw new TranscriptError(err.message, "blocked");
|
|
245
|
+
}
|
|
246
|
+
// Direct read as a last resort. It returns nothing today, but costs one
|
|
247
|
+
// request and starts working again the moment YouTube relaxes this.
|
|
248
|
+
const xml = await fetchText(track.url, timeoutMs);
|
|
249
|
+
segments = parseTimedText(xml);
|
|
250
|
+
if (segments.length === 0) {
|
|
251
|
+
throw new TranscriptError(err instanceof Error ? err.message : "Could not download the caption track.", "blocked");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (segments.length === 0) {
|
|
255
|
+
throw new TranscriptError("The caption track was empty.", "no_captions");
|
|
256
|
+
}
|
|
257
|
+
return { videoId, language, autoGenerated, segments };
|
|
258
|
+
}
|
|
259
|
+
/** `1h02m03s` style stamps read better than raw seconds in a transcript dump. */
|
|
260
|
+
export function formatTimestamp(seconds) {
|
|
261
|
+
const total = Math.floor(seconds);
|
|
262
|
+
const h = Math.floor(total / 3600);
|
|
263
|
+
const m = Math.floor((total % 3600) / 60);
|
|
264
|
+
const s = total % 60;
|
|
265
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
266
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
|
|
267
|
+
}
|
|
268
|
+
/** Collapse segments into readable prose. Auto-captions arrive as ~2 second fragments. */
|
|
269
|
+
export function toPlainText(segments) {
|
|
270
|
+
return segments
|
|
271
|
+
.map((s) => s.text)
|
|
272
|
+
.join(" ")
|
|
273
|
+
.replace(/\s+/g, " ")
|
|
274
|
+
.trim();
|
|
275
|
+
}
|
|
276
|
+
/** Timestamped lines, grouped so the output is scannable rather than one line per fragment. */
|
|
277
|
+
export function toTimestampedText(segments, groupSeconds = 30) {
|
|
278
|
+
const lines = [];
|
|
279
|
+
let bucketStart = null;
|
|
280
|
+
let buffer = [];
|
|
281
|
+
const flush = () => {
|
|
282
|
+
if (bucketStart !== null && buffer.length) {
|
|
283
|
+
lines.push(`[${formatTimestamp(bucketStart)}] ${buffer.join(" ").replace(/\s+/g, " ").trim()}`);
|
|
284
|
+
}
|
|
285
|
+
buffer = [];
|
|
286
|
+
};
|
|
287
|
+
for (const seg of segments) {
|
|
288
|
+
if (bucketStart === null)
|
|
289
|
+
bucketStart = seg.start;
|
|
290
|
+
if (seg.start - bucketStart >= groupSeconds) {
|
|
291
|
+
flush();
|
|
292
|
+
bucketStart = seg.start;
|
|
293
|
+
}
|
|
294
|
+
buffer.push(seg.text);
|
|
295
|
+
}
|
|
296
|
+
flush();
|
|
297
|
+
return lines.join("\n");
|
|
298
|
+
}
|
|
299
|
+
/** Find a phrase in a transcript and return the timestamps where it occurs. */
|
|
300
|
+
export function searchSegments(segments, query, contextSegments = 1) {
|
|
301
|
+
const needle = query.toLowerCase().trim();
|
|
302
|
+
if (!needle)
|
|
303
|
+
return [];
|
|
304
|
+
const hits = [];
|
|
305
|
+
for (let i = 0; i < segments.length; i++) {
|
|
306
|
+
const seg = segments[i];
|
|
307
|
+
if (!seg || !seg.text.toLowerCase().includes(needle))
|
|
308
|
+
continue;
|
|
309
|
+
const from = Math.max(0, i - contextSegments);
|
|
310
|
+
const to = Math.min(segments.length, i + contextSegments + 1);
|
|
311
|
+
const text = segments
|
|
312
|
+
.slice(from, to)
|
|
313
|
+
.map((s) => s.text)
|
|
314
|
+
.join(" ")
|
|
315
|
+
.replace(/\s+/g, " ")
|
|
316
|
+
.trim();
|
|
317
|
+
hits.push({ timestamp: formatTimestamp(seg.start), start: seg.start, text });
|
|
318
|
+
}
|
|
319
|
+
return hits;
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=transcripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcripts.js","sourceRoot":"","sources":["../../src/youtube/transcripts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* yt-dlp transport for caption tracks.
|
|
3
|
+
*
|
|
4
|
+
* As of 2026 YouTube answers a bare `timedtext` URL with 200 and an empty body
|
|
5
|
+
* unless the request carries a proof-of-origin token tied to a real player
|
|
6
|
+
* session. Scraping the watch page still lists which tracks exist, but it can
|
|
7
|
+
* no longer read their contents, so the track list and the track body now come
|
|
8
|
+
* from different places.
|
|
9
|
+
*
|
|
10
|
+
* yt-dlp already solves the hard half and is maintained against YouTube's
|
|
11
|
+
* changes far faster than this repo could be, so we shell out to it rather than
|
|
12
|
+
* reimplement token minting. It is an optional dependency: without it you can
|
|
13
|
+
* still list languages, and every other tool in this server is unaffected.
|
|
14
|
+
*/
|
|
15
|
+
import type { TranscriptSegment } from "./transcripts.js";
|
|
16
|
+
export declare class YtDlpMissingError extends Error {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
/** True when yt-dlp can actually be launched. Used by `doctor` and by tool errors. */
|
|
20
|
+
export declare function isAvailable(): Promise<boolean>;
|
|
21
|
+
export type YtDlpTranscript = {
|
|
22
|
+
segments: TranscriptSegment[];
|
|
23
|
+
/** The language actually written out, which may be a regional variant of what was asked for. */
|
|
24
|
+
language: string;
|
|
25
|
+
autoGenerated: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Download one caption track.
|
|
29
|
+
*
|
|
30
|
+
* `--sub-langs` accepts a glob, so `en.*` picks up `en`, `en-GB` and the
|
|
31
|
+
* auto-generated `en-orig` in one pass. We take whichever file lands and report
|
|
32
|
+
* the language from its name rather than assuming we got the exact code asked
|
|
33
|
+
* for.
|
|
34
|
+
*/
|
|
35
|
+
export declare function fetchCaptions(videoId: string, language?: string, timeoutMs?: number): Promise<YtDlpTranscript>;
|