@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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +877 -0
  3. package/SKILL.md +162 -0
  4. package/dist/accounts/store.d.ts +41 -0
  5. package/dist/accounts/store.js +110 -0
  6. package/dist/accounts/store.js.map +1 -0
  7. package/dist/auth.d.ts +14 -0
  8. package/dist/auth.js +206 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/cli.d.ts +55 -0
  11. package/dist/cli.js +439 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/config.d.ts +42 -0
  14. package/dist/config.js +130 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/doctor.d.ts +7 -0
  17. package/dist/doctor.js +78 -0
  18. package/dist/doctor.js.map +1 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.js +134 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/safety.d.ts +50 -0
  23. package/dist/safety.js +84 -0
  24. package/dist/safety.js.map +1 -0
  25. package/dist/server.d.ts +10 -0
  26. package/dist/server.js +28 -0
  27. package/dist/server.js.map +1 -0
  28. package/dist/tools/account.d.ts +10 -0
  29. package/dist/tools/account.js +222 -0
  30. package/dist/tools/account.js.map +1 -0
  31. package/dist/tools/index.d.ts +9 -0
  32. package/dist/tools/index.js +12 -0
  33. package/dist/tools/index.js.map +1 -0
  34. package/dist/tools/kit.d.ts +57 -0
  35. package/dist/tools/kit.js +78 -0
  36. package/dist/tools/kit.js.map +1 -0
  37. package/dist/tools/research.d.ts +16 -0
  38. package/dist/tools/research.js +232 -0
  39. package/dist/tools/research.js.map +1 -0
  40. package/dist/tools/transcripts.d.ts +9 -0
  41. package/dist/tools/transcripts.js +123 -0
  42. package/dist/tools/transcripts.js.map +1 -0
  43. package/dist/tools/types.d.ts +6 -0
  44. package/dist/tools/types.js +2 -0
  45. package/dist/tools/types.js.map +1 -0
  46. package/dist/transport/http.d.ts +23 -0
  47. package/dist/transport/http.js +83 -0
  48. package/dist/transport/http.js.map +1 -0
  49. package/dist/youtube/api.d.ts +47 -0
  50. package/dist/youtube/api.js +176 -0
  51. package/dist/youtube/api.js.map +1 -0
  52. package/dist/youtube/transcripts.d.ts +65 -0
  53. package/dist/youtube/transcripts.js +321 -0
  54. package/dist/youtube/transcripts.js.map +1 -0
  55. package/dist/youtube/ytdlp.d.ts +35 -0
  56. package/dist/youtube/ytdlp.js +145 -0
  57. package/dist/youtube/ytdlp.js.map +1 -0
  58. 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(/&amp;#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
65
+ .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
66
+ .replace(/&amp;quot;/g, '"')
67
+ .replace(/&quot;/g, '"')
68
+ .replace(/&amp;#39;/g, "'")
69
+ .replace(/&#39;/g, "'")
70
+ .replace(/&amp;lt;/g, "<")
71
+ .replace(/&lt;/g, "<")
72
+ .replace(/&amp;gt;/g, ">")
73
+ .replace(/&gt;/g, ">")
74
+ .replace(/&amp;nbsp;/g, " ")
75
+ .replace(/&nbsp;/g, " ")
76
+ .replace(/&amp;/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>;