@vitkuz/youtube-adapter 1.1.0 → 1.1.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/dist/index.d.mts CHANGED
@@ -44,6 +44,16 @@ interface TranscriptItem {
44
44
  type GetTranscriptOutput = TranscriptItem[];
45
45
  declare const getTranscript: (context: Context) => (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;
46
46
 
47
+ interface GetTranscriptHtmlInput {
48
+ videoId: string;
49
+ lang?: string;
50
+ }
51
+ interface GetTranscriptHtmlOutput {
52
+ html: string;
53
+ segments: TranscriptItem[];
54
+ }
55
+ declare const getTranscriptHtml: (context: Context) => (input: GetTranscriptHtmlInput) => Promise<GetTranscriptHtmlOutput>;
56
+
47
57
  interface AdapterConfig {
48
58
  apiKey: string;
49
59
  firecrawlApiKey?: string;
@@ -55,7 +65,8 @@ interface Adapter {
55
65
  videoDetails: (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;
56
66
  getAllChannelVideos: (input: GetAllChannelVideosInput) => Promise<GetAllChannelVideosOutput>;
57
67
  getTranscript: (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;
68
+ getTranscriptHtml: (input: GetTranscriptHtmlInput) => Promise<GetTranscriptHtmlOutput>;
58
69
  }
59
70
  declare const createAdapter: (config: AdapterConfig) => Adapter;
60
71
 
61
- export { type Adapter, type AdapterConfig, type Context, type GetTranscriptInput, type GetTranscriptOutput, type Logger, type SearchInput, type SearchOutput, type TranscriptItem, type VideoDetailsInput, type VideoDetailsOutput, type YoutubeClient, createAdapter, createClient, getTranscript, search, videoDetails };
72
+ export { type Adapter, type AdapterConfig, type Context, type GetTranscriptHtmlInput, type GetTranscriptHtmlOutput, type GetTranscriptInput, type GetTranscriptOutput, type Logger, type SearchInput, type SearchOutput, type TranscriptItem, type VideoDetailsInput, type VideoDetailsOutput, type YoutubeClient, createAdapter, createClient, getTranscript, getTranscriptHtml, search, videoDetails };
package/dist/index.d.ts CHANGED
@@ -44,6 +44,16 @@ interface TranscriptItem {
44
44
  type GetTranscriptOutput = TranscriptItem[];
45
45
  declare const getTranscript: (context: Context) => (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;
46
46
 
47
+ interface GetTranscriptHtmlInput {
48
+ videoId: string;
49
+ lang?: string;
50
+ }
51
+ interface GetTranscriptHtmlOutput {
52
+ html: string;
53
+ segments: TranscriptItem[];
54
+ }
55
+ declare const getTranscriptHtml: (context: Context) => (input: GetTranscriptHtmlInput) => Promise<GetTranscriptHtmlOutput>;
56
+
47
57
  interface AdapterConfig {
48
58
  apiKey: string;
49
59
  firecrawlApiKey?: string;
@@ -55,7 +65,8 @@ interface Adapter {
55
65
  videoDetails: (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;
56
66
  getAllChannelVideos: (input: GetAllChannelVideosInput) => Promise<GetAllChannelVideosOutput>;
57
67
  getTranscript: (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;
68
+ getTranscriptHtml: (input: GetTranscriptHtmlInput) => Promise<GetTranscriptHtmlOutput>;
58
69
  }
59
70
  declare const createAdapter: (config: AdapterConfig) => Adapter;
60
71
 
61
- export { type Adapter, type AdapterConfig, type Context, type GetTranscriptInput, type GetTranscriptOutput, type Logger, type SearchInput, type SearchOutput, type TranscriptItem, type VideoDetailsInput, type VideoDetailsOutput, type YoutubeClient, createAdapter, createClient, getTranscript, search, videoDetails };
72
+ export { type Adapter, type AdapterConfig, type Context, type GetTranscriptHtmlInput, type GetTranscriptHtmlOutput, type GetTranscriptInput, type GetTranscriptOutput, type Logger, type SearchInput, type SearchOutput, type TranscriptItem, type VideoDetailsInput, type VideoDetailsOutput, type YoutubeClient, createAdapter, createClient, getTranscript, getTranscriptHtml, search, videoDetails };
package/dist/index.js CHANGED
@@ -117,7 +117,11 @@ var getTranscript = (context) => async (input) => {
117
117
  const response = await firecrawlAdapter.scrape({
118
118
  url,
119
119
  params: {
120
- formats: ["markdown"]
120
+ formats: ["markdown"],
121
+ // Attempt to bypass blocks
122
+ waitFor: 5e3
123
+ // Try to request stealth proxy if supported by the plan/SDK
124
+ // Note: 'proxy' param availability depends on Firecrawl version/plan
121
125
  }
122
126
  });
123
127
  if (!response.success || !response.data || !response.data.markdown) {
@@ -130,19 +134,25 @@ var getTranscript = (context) => async (input) => {
130
134
  logger?.debug("getTranscript:error", { error });
131
135
  throw new Error(`Failed to scrape YouTube page: ${error.message || String(error)}`);
132
136
  }
133
- const segments = [];
137
+ let transcriptContent = "";
134
138
  const transcriptStartValues = markdown.split("## Transcript");
135
- if (transcriptStartValues.length < 2) {
136
- if (markdown.length < 500) {
137
- logger?.debug("getTranscript:markdown-too-short", { data: { markdown } });
139
+ if (transcriptStartValues.length >= 2) {
140
+ transcriptContent = transcriptStartValues[1];
141
+ } else {
142
+ const showTranscriptSplit = markdown.split("Show transcript");
143
+ if (showTranscriptSplit.length >= 2) {
144
+ transcriptContent = showTranscriptSplit[showTranscriptSplit.length - 1];
145
+ logger?.debug("getTranscript:using-fallback-header", {
146
+ data: { header: "Show transcript" }
147
+ });
138
148
  } else {
139
- logger?.debug("getTranscript:no-transcript-found", {
140
- data: { preview: markdown.slice(0, 1e3) }
149
+ logger?.warn("getTranscript:no-transcript-header-found", {
150
+ data: { msg: "Attempting to parse full markdown" }
141
151
  });
152
+ transcriptContent = markdown;
142
153
  }
143
- throw new Error("Transcript section not found in scraped content");
144
154
  }
145
- const transcriptContent = transcriptStartValues[1];
155
+ const segments = [];
146
156
  const lines = transcriptContent.split("\n");
147
157
  let currentTimestamp = "";
148
158
  let currentText = [];
@@ -162,9 +172,11 @@ var getTranscript = (context) => async (input) => {
162
172
  } else {
163
173
  if (trimmed.startsWith("##")) continue;
164
174
  if (trimmed.startsWith("[![](")) {
165
- break;
175
+ if (segments.length > 0) {
176
+ break;
177
+ }
166
178
  }
167
- if (trimmed === "English" || trimmed === "German") {
179
+ if (trimmed === "English" || trimmed === "German" || trimmed === "Auto-dubbed") {
168
180
  continue;
169
181
  }
170
182
  if (currentTimestamp) {
@@ -178,9 +190,192 @@ var getTranscript = (context) => async (input) => {
178
190
  text: currentText.join(" ").trim()
179
191
  });
180
192
  }
193
+ if (segments.length === 0) {
194
+ throw new Error("Transcript parsing failed: No segments found after parsing.");
195
+ }
181
196
  logger?.debug("getTranscript:success", { data: { count: segments.length } });
182
197
  return segments;
183
198
  };
199
+
200
+ // src/operations/get-transcript-html.ts
201
+ var INNERTUBE = {
202
+ // Public API key embedded in YouTube's JavaScript - may change over time
203
+ DEFAULT_API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
204
+ API_URL: "https://www.youtube.com/youtubei/v1/player",
205
+ CLIENT_VERSION: "2.20250222.10.00"
206
+ };
207
+ var currentApiKey = INNERTUBE.DEFAULT_API_KEY;
208
+ var fetchFreshApiKey = async (logger) => {
209
+ logger?.debug("Fetching fresh API key from YouTube...");
210
+ const response = await fetch("https://www.youtube.com", {
211
+ headers: {
212
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
213
+ }
214
+ });
215
+ if (!response.ok) {
216
+ throw new Error(`Failed to fetch YouTube homepage: ${response.status}`);
217
+ }
218
+ const html = await response.text();
219
+ const patterns = [
220
+ /"INNERTUBE_API_KEY":"([^"]+)"/,
221
+ /innertubeApiKey":"([^"]+)"/,
222
+ /api_key=([A-Za-z0-9_-]+)/
223
+ ];
224
+ for (const pattern of patterns) {
225
+ const match = html.match(pattern);
226
+ if (match && match[1]) {
227
+ logger?.debug(`Found new API key: ${match[1].slice(0, 10)}...`);
228
+ return match[1];
229
+ }
230
+ }
231
+ throw new Error("Could not find API key in YouTube page");
232
+ };
233
+ var generateVisitorData = () => {
234
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
235
+ return Array.from(
236
+ { length: 11 },
237
+ () => chars.charAt(Math.floor(Math.random() * chars.length))
238
+ ).join("");
239
+ };
240
+ var decodeHtmlEntities = (text) => {
241
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&apos;/g, "'").replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10))).replace(/&#x([a-fA-F0-9]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
242
+ };
243
+ var getPlayerResponse = async (videoId, apiKey) => {
244
+ const visitorData = generateVisitorData();
245
+ const response = await fetch(`${INNERTUBE.API_URL}?key=${apiKey}`, {
246
+ method: "POST",
247
+ headers: {
248
+ "Content-Type": "application/json",
249
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
250
+ "X-Youtube-Client-Version": INNERTUBE.CLIENT_VERSION,
251
+ "X-Youtube-Client-Name": "1",
252
+ "X-Goog-Visitor-Id": visitorData,
253
+ Origin: "https://www.youtube.com",
254
+ Referer: "https://www.youtube.com/"
255
+ },
256
+ body: JSON.stringify({
257
+ context: {
258
+ client: {
259
+ hl: "en",
260
+ gl: "US",
261
+ clientName: "WEB",
262
+ clientVersion: INNERTUBE.CLIENT_VERSION,
263
+ visitorData
264
+ }
265
+ },
266
+ videoId,
267
+ racyCheckOk: true,
268
+ contentCheckOk: true
269
+ })
270
+ });
271
+ if (!response.ok) {
272
+ throw new Error(`API request failed: ${response.status}`);
273
+ }
274
+ return response.json();
275
+ };
276
+ var fetchCaptionXml = async (baseUrl, videoId) => {
277
+ const url = baseUrl.replace("&fmt=srv3", "");
278
+ const response = await fetch(url, {
279
+ headers: {
280
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
281
+ Referer: `https://www.youtube.com/watch?v=${videoId}`
282
+ }
283
+ });
284
+ if (!response.ok) {
285
+ throw new Error(`Caption fetch failed: ${response.status}`);
286
+ }
287
+ return response.text();
288
+ };
289
+ var parseXmlCaptions = (xml) => {
290
+ if (!xml.includes("<text")) return [];
291
+ return xml.split("</text>").filter((line) => line.includes("<text")).map((line) => {
292
+ const startMatch = /start="([\d.]+)"/.exec(line);
293
+ const durMatch = /dur="([\d.]+)"/.exec(line);
294
+ const textMatch = /<text[^>]*>(.+)$/s.exec(line);
295
+ if (!startMatch || !durMatch || !textMatch) return null;
296
+ const rawText = textMatch[1].replace(/<[^>]*>/g, "").trim();
297
+ const text = decodeHtmlEntities(rawText);
298
+ return {
299
+ start: parseFloat(startMatch[1]),
300
+ duration: parseFloat(durMatch[1]),
301
+ text
302
+ };
303
+ }).filter((e) => e !== null && e.text.length > 0);
304
+ };
305
+ var formatTime = (seconds) => {
306
+ const h = Math.floor(seconds / 3600);
307
+ const m = Math.floor(seconds % 3600 / 60);
308
+ const s = Math.floor(seconds % 60);
309
+ if (h > 0) {
310
+ return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
311
+ }
312
+ return `${m}:${String(s).padStart(2, "0")}`;
313
+ };
314
+ var tryExtractSubtitles = async (videoId, apiKey, logger) => {
315
+ const playerData = await getPlayerResponse(videoId, apiKey);
316
+ if (playerData.playabilityStatus?.status !== "OK") {
317
+ throw new Error(`Video not playable: ${playerData.playabilityStatus?.status}`);
318
+ }
319
+ const tracks = playerData.captions?.playerCaptionsTracklistRenderer?.captionTracks || [];
320
+ if (tracks.length === 0) {
321
+ throw new Error("No subtitles available for this video");
322
+ }
323
+ const track = tracks[0];
324
+ logger?.debug(`Found caption track: ${track.name?.simpleText} (${track.languageCode})`);
325
+ const xml = await fetchCaptionXml(track.baseUrl, videoId);
326
+ const subtitles = parseXmlCaptions(xml);
327
+ if (subtitles.length === 0) {
328
+ throw new Error("Failed to parse subtitles");
329
+ }
330
+ return subtitles.map((s) => ({
331
+ timestamp: formatTime(s.start),
332
+ text: s.text
333
+ }));
334
+ };
335
+ var getTranscriptHtml = (context) => async (input) => {
336
+ const { logger } = context;
337
+ logger?.debug("getTranscriptHtml:start", { data: input });
338
+ const videoId = input.videoId;
339
+ const MAX_ATTEMPTS = 3;
340
+ let lastError = null;
341
+ let segments = [];
342
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
343
+ try {
344
+ logger?.debug(
345
+ `Attempt ${attempt}/${MAX_ATTEMPTS} (API key: ${currentApiKey.slice(0, 10)}...)`
346
+ );
347
+ segments = await tryExtractSubtitles(videoId, currentApiKey, logger);
348
+ break;
349
+ } catch (error) {
350
+ lastError = error instanceof Error ? error : new Error(String(error));
351
+ logger?.warn(`Attempt ${attempt} failed: ${lastError.message}`);
352
+ if (attempt < MAX_ATTEMPTS) {
353
+ try {
354
+ currentApiKey = await fetchFreshApiKey(logger);
355
+ logger?.debug("Retrying with new API key...");
356
+ } catch (keyError) {
357
+ logger?.warn(`Failed to fetch new API key: ${keyError}`);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ if (segments.length === 0) {
363
+ throw new Error(
364
+ `Failed after ${MAX_ATTEMPTS} attempts. Last error: ${lastError?.message}`
365
+ );
366
+ }
367
+ const htmlParts = segments.map(
368
+ (s) => `<div class="segment"><span class="timestamp">${s.timestamp}</span><span class="text">${s.text}</span></div>`
369
+ );
370
+ const html = `<html><body><div class="transcript">${htmlParts.join("\n")}</div></body></html>`;
371
+ logger?.debug("getTranscriptHtml:success", {
372
+ data: { count: segments.length }
373
+ });
374
+ return {
375
+ html,
376
+ segments
377
+ };
378
+ };
184
379
  var createAdapter = (config) => {
185
380
  const client = createClient(config.apiKey);
186
381
  let firecrawlAdapter$1;
@@ -202,13 +397,15 @@ var createAdapter = (config) => {
202
397
  search: search(context),
203
398
  videoDetails: videoDetails(context),
204
399
  getAllChannelVideos: getAllChannelVideos(context),
205
- getTranscript: getTranscript(context)
400
+ getTranscript: getTranscript(context),
401
+ getTranscriptHtml: getTranscriptHtml(context)
206
402
  };
207
403
  };
208
404
 
209
405
  exports.createAdapter = createAdapter;
210
406
  exports.createClient = createClient;
211
407
  exports.getTranscript = getTranscript;
408
+ exports.getTranscriptHtml = getTranscriptHtml;
212
409
  exports.search = search;
213
410
  exports.videoDetails = videoDetails;
214
411
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/operations/search.ts","../src/operations/video-details.ts","../src/operations/get-all-channel-videos.ts","../src/operations/get-transcript.ts","../src/adapter.ts"],"names":["google","firecrawlAdapter","createFirecrawlAdapter","FirecrawlPlan"],"mappings":"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","file":"index.js","sourcesContent":["import { google, youtube_v3 } from 'googleapis';\n\nexport type YoutubeClient = youtube_v3.Youtube;\n\nexport const createClient = (apiKey: string): YoutubeClient => {\n return google.youtube({\n version: 'v3',\n auth: apiKey,\n });\n};\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type SearchInput = youtube_v3.Params$Resource$Search$List;\nexport type SearchOutput = youtube_v3.Schema$SearchListResponse;\n\nexport const search =\n (context: Context) =>\n async (input: SearchInput): Promise<SearchOutput> => {\n const { client, logger } = context;\n\n logger?.debug('search:start', { data: input });\n\n try {\n const response = await client.search.list({\n part: ['snippet'],\n ...input,\n });\n\n logger?.debug('search:success');\n return response.data;\n } catch (error) {\n logger?.debug('search:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type VideoDetailsInput = youtube_v3.Params$Resource$Videos$List;\nexport type VideoDetailsOutput = youtube_v3.Schema$VideoListResponse;\n\nexport const videoDetails =\n (context: Context) =>\n async (input: VideoDetailsInput): Promise<VideoDetailsOutput> => {\n const { client, logger } = context;\n\n logger?.debug('videoDetails:start', { data: input });\n\n try {\n const response = await client.videos.list({\n part: ['snippet', 'contentDetails', 'statistics'],\n ...input,\n });\n\n logger?.debug('videoDetails:success');\n return response.data;\n } catch (error) {\n logger?.debug('videoDetails:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport interface GetAllChannelVideosInput {\n channelId: string;\n}\n\nexport interface GetAllChannelVideosOutput {\n items: youtube_v3.Schema$Video[];\n totalCount: number;\n}\n\nexport const getAllChannelVideos =\n (context: Context) =>\n async (input: GetAllChannelVideosInput): Promise<GetAllChannelVideosOutput> => {\n const { client, logger } = context;\n\n logger?.debug('getAllChannelVideos:start', { data: input });\n\n try {\n // Step 1: Get the Uploads playlist ID\n const channelResponse = await client.channels.list({\n id: [input.channelId],\n part: ['contentDetails'],\n });\n\n const uploadsPlaylistId =\n channelResponse.data.items?.[0]?.contentDetails?.relatedPlaylists?.uploads;\n\n if (!uploadsPlaylistId) {\n throw new Error(\n `Could not find uploads playlist for channel ID: ${input.channelId}`,\n );\n }\n\n logger?.debug('getAllChannelVideos:found_playlist', { data: { uploadsPlaylistId } });\n\n // Step 2: Recursively fetch all playlist items and their video details\n let items: youtube_v3.Schema$Video[] = [];\n let nextPageToken: string | undefined = undefined;\n\n do {\n // Get playlist items (Video IDs)\n const playlistResponse: { data: youtube_v3.Schema$PlaylistItemListResponse } =\n (await client.playlistItems.list({\n playlistId: uploadsPlaylistId,\n part: ['contentDetails'],\n maxResults: 50,\n pageToken: nextPageToken,\n })) as any;\n\n const playlistItems = playlistResponse.data.items || [];\n\n if (playlistItems.length > 0) {\n const videoIds = playlistItems\n .map((item) => item.contentDetails?.videoId)\n .filter((id): id is string => !!id);\n\n if (videoIds.length > 0) {\n // Fetch full video details\n const videosResponse = await client.videos.list({\n id: videoIds,\n part: ['snippet', 'contentDetails', 'statistics'],\n });\n\n const videoItems = videosResponse.data.items || [];\n items = items.concat(videoItems);\n }\n }\n\n nextPageToken = playlistResponse.data.nextPageToken || undefined;\n\n logger?.debug('getAllChannelVideos:fetched_page', {\n data: {\n fetched: playlistItems.length,\n totalVideosSoFar: items.length,\n hasNextPage: !!nextPageToken,\n },\n });\n } while (nextPageToken);\n\n logger?.debug('getAllChannelVideos:success', { data: { totalCount: items.length } });\n\n return {\n items,\n totalCount: items.length,\n };\n } catch (error) {\n logger?.debug('getAllChannelVideos:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\n\nexport interface GetTranscriptInput {\n videoId: string;\n lang?: string;\n}\n\nexport interface TranscriptItem {\n timestamp: string;\n text: string;\n}\n\nexport type GetTranscriptOutput = TranscriptItem[];\n\nexport const getTranscript =\n (context: Context) =>\n async (input: GetTranscriptInput): Promise<GetTranscriptOutput> => {\n const { logger, firecrawlAdapter } = context;\n logger?.debug('getTranscript:start', { data: input });\n\n if (!firecrawlAdapter) {\n throw new Error(\n 'Firecrawl adapter is not initialized. Provide firecrawlApiKey in config.',\n );\n }\n\n const url = `https://www.youtube.com/watch?v=${input.videoId}`;\n let markdown: string;\n\n try {\n // Use firecrawlAdapter.scrape directly\n const response = await firecrawlAdapter.scrape({\n url,\n params: {\n formats: ['markdown'],\n },\n });\n\n if (!response.success || !response.data || !response.data.markdown) {\n throw new Error(\n `Failed to scrape YouTube page: ${response.error || 'No data returned'}`,\n );\n }\n markdown = response.data.markdown;\n } catch (error: any) {\n logger?.debug('getTranscript:error', { error });\n throw new Error(`Failed to scrape YouTube page: ${error.message || String(error)}`);\n }\n\n // Parsing logic reused from firecrawl adapter implementation\n const segments: TranscriptItem[] = [];\n\n const transcriptStartValues = markdown.split('## Transcript');\n if (transcriptStartValues.length < 2) {\n if (markdown.length < 500) {\n logger?.debug('getTranscript:markdown-too-short', { data: { markdown } });\n } else {\n logger?.debug('getTranscript:no-transcript-found', {\n data: { preview: markdown.slice(0, 1000) },\n });\n }\n throw new Error('Transcript section not found in scraped content');\n }\n\n const transcriptContent = transcriptStartValues[1];\n\n const lines = transcriptContent.split('\\n');\n let currentTimestamp = '';\n let currentText: string[] = [];\n\n const timestampRegex = /^(\\d{1,2}:)?\\d{1,2}:\\d{2}$/; // Matches 0:01, 10:05, 1:00:00\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n // Check if it's a timestamp\n if (timestampRegex.test(trimmed)) {\n // If we have a previous segment accumulating, push it\n if (currentTimestamp) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n currentText = [];\n }\n currentTimestamp = trimmed;\n } else {\n // It's text or a header?\n if (trimmed.startsWith('##')) continue;\n\n // Stop if we hit video thumbnails (footer)\n if (trimmed.startsWith('[![](')) {\n break;\n }\n\n // Stop if we hit language options (English/German) which usually signify end of transcript\n if (trimmed === 'English' || trimmed === 'German') {\n continue;\n }\n\n if (currentTimestamp) {\n currentText.push(trimmed);\n }\n }\n }\n\n // Push last segment\n if (currentTimestamp && currentText.length > 0) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n }\n\n logger?.debug('getTranscript:success', { data: { count: segments.length } });\n return segments;\n };\n","import { createClient, YoutubeClient } from './client';\nimport { Context, Logger } from './types';\nimport { search, SearchInput, SearchOutput } from './operations/search';\nimport { videoDetails, VideoDetailsInput, VideoDetailsOutput } from './operations/video-details';\nimport {\n getAllChannelVideos,\n GetAllChannelVideosInput,\n GetAllChannelVideosOutput,\n} from './operations/get-all-channel-videos';\nimport {\n getTranscript,\n GetTranscriptInput,\n GetTranscriptOutput,\n} from './operations/get-transcript';\n\nexport interface AdapterConfig {\n apiKey: string;\n firecrawlApiKey?: string;\n logger?: Logger;\n}\n\nexport interface Adapter {\n client: YoutubeClient;\n search: (input: SearchInput) => Promise<SearchOutput>;\n videoDetails: (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;\n getAllChannelVideos: (input: GetAllChannelVideosInput) => Promise<GetAllChannelVideosOutput>;\n getTranscript: (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;\n}\n\nimport { createAdapter as createFirecrawlAdapter, FirecrawlPlan } from '@vitkuz/firecrawl-adapter';\n\nexport const createAdapter = (config: AdapterConfig): Adapter => {\n const client = createClient(config.apiKey);\n\n let firecrawlAdapter;\n if (config.firecrawlApiKey) {\n firecrawlAdapter = createFirecrawlAdapter({\n apiKey: config.firecrawlApiKey,\n plan: FirecrawlPlan.FREE, // Default to free, could be configurable\n logger: config.logger,\n });\n }\n\n const context: Context = {\n client,\n firecrawlAdapter,\n logger: config.logger,\n };\n\n return {\n client,\n search: search(context),\n videoDetails: videoDetails(context),\n getAllChannelVideos: getAllChannelVideos(context),\n getTranscript: getTranscript(context),\n };\n};\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/operations/search.ts","../src/operations/video-details.ts","../src/operations/get-all-channel-videos.ts","../src/operations/get-transcript.ts","../src/operations/get-transcript-html.ts","../src/adapter.ts"],"names":["google","firecrawlAdapter","createFirecrawlAdapter","FirecrawlPlan"],"mappings":"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","file":"index.js","sourcesContent":["import { google, youtube_v3 } from 'googleapis';\n\nexport type YoutubeClient = youtube_v3.Youtube;\n\nexport const createClient = (apiKey: string): YoutubeClient => {\n return google.youtube({\n version: 'v3',\n auth: apiKey,\n });\n};\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type SearchInput = youtube_v3.Params$Resource$Search$List;\nexport type SearchOutput = youtube_v3.Schema$SearchListResponse;\n\nexport const search =\n (context: Context) =>\n async (input: SearchInput): Promise<SearchOutput> => {\n const { client, logger } = context;\n\n logger?.debug('search:start', { data: input });\n\n try {\n const response = await client.search.list({\n part: ['snippet'],\n ...input,\n });\n\n logger?.debug('search:success');\n return response.data;\n } catch (error) {\n logger?.debug('search:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type VideoDetailsInput = youtube_v3.Params$Resource$Videos$List;\nexport type VideoDetailsOutput = youtube_v3.Schema$VideoListResponse;\n\nexport const videoDetails =\n (context: Context) =>\n async (input: VideoDetailsInput): Promise<VideoDetailsOutput> => {\n const { client, logger } = context;\n\n logger?.debug('videoDetails:start', { data: input });\n\n try {\n const response = await client.videos.list({\n part: ['snippet', 'contentDetails', 'statistics'],\n ...input,\n });\n\n logger?.debug('videoDetails:success');\n return response.data;\n } catch (error) {\n logger?.debug('videoDetails:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport interface GetAllChannelVideosInput {\n channelId: string;\n}\n\nexport interface GetAllChannelVideosOutput {\n items: youtube_v3.Schema$Video[];\n totalCount: number;\n}\n\nexport const getAllChannelVideos =\n (context: Context) =>\n async (input: GetAllChannelVideosInput): Promise<GetAllChannelVideosOutput> => {\n const { client, logger } = context;\n\n logger?.debug('getAllChannelVideos:start', { data: input });\n\n try {\n // Step 1: Get the Uploads playlist ID\n const channelResponse = await client.channels.list({\n id: [input.channelId],\n part: ['contentDetails'],\n });\n\n const uploadsPlaylistId =\n channelResponse.data.items?.[0]?.contentDetails?.relatedPlaylists?.uploads;\n\n if (!uploadsPlaylistId) {\n throw new Error(\n `Could not find uploads playlist for channel ID: ${input.channelId}`,\n );\n }\n\n logger?.debug('getAllChannelVideos:found_playlist', { data: { uploadsPlaylistId } });\n\n // Step 2: Recursively fetch all playlist items and their video details\n let items: youtube_v3.Schema$Video[] = [];\n let nextPageToken: string | undefined = undefined;\n\n do {\n // Get playlist items (Video IDs)\n const playlistResponse: { data: youtube_v3.Schema$PlaylistItemListResponse } =\n (await client.playlistItems.list({\n playlistId: uploadsPlaylistId,\n part: ['contentDetails'],\n maxResults: 50,\n pageToken: nextPageToken,\n })) as any;\n\n const playlistItems = playlistResponse.data.items || [];\n\n if (playlistItems.length > 0) {\n const videoIds = playlistItems\n .map((item) => item.contentDetails?.videoId)\n .filter((id): id is string => !!id);\n\n if (videoIds.length > 0) {\n // Fetch full video details\n const videosResponse = await client.videos.list({\n id: videoIds,\n part: ['snippet', 'contentDetails', 'statistics'],\n });\n\n const videoItems = videosResponse.data.items || [];\n items = items.concat(videoItems);\n }\n }\n\n nextPageToken = playlistResponse.data.nextPageToken || undefined;\n\n logger?.debug('getAllChannelVideos:fetched_page', {\n data: {\n fetched: playlistItems.length,\n totalVideosSoFar: items.length,\n hasNextPage: !!nextPageToken,\n },\n });\n } while (nextPageToken);\n\n logger?.debug('getAllChannelVideos:success', { data: { totalCount: items.length } });\n\n return {\n items,\n totalCount: items.length,\n };\n } catch (error) {\n logger?.debug('getAllChannelVideos:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\n\nexport interface GetTranscriptInput {\n videoId: string;\n lang?: string;\n}\n\nexport interface TranscriptItem {\n timestamp: string;\n text: string;\n}\n\nexport type GetTranscriptOutput = TranscriptItem[];\n\nexport const getTranscript =\n (context: Context) =>\n async (input: GetTranscriptInput): Promise<GetTranscriptOutput> => {\n const { logger, firecrawlAdapter } = context;\n logger?.debug('getTranscript:start', { data: input });\n\n if (!firecrawlAdapter) {\n throw new Error(\n 'Firecrawl adapter is not initialized. Provide firecrawlApiKey in config.',\n );\n }\n\n const url = `https://www.youtube.com/watch?v=${input.videoId}`;\n let markdown: string;\n\n try {\n // Use firecrawlAdapter.scrape directly\n const response = await firecrawlAdapter.scrape({\n url,\n params: {\n formats: ['markdown'],\n // Attempt to bypass blocks\n waitFor: 5000,\n // Try to request stealth proxy if supported by the plan/SDK\n // Note: 'proxy' param availability depends on Firecrawl version/plan\n },\n });\n\n if (!response.success || !response.data || !response.data.markdown) {\n // If 403 or other error, it might be in response.error or just failure\n throw new Error(\n `Failed to scrape YouTube page: ${response.error || 'No data returned'}`,\n );\n }\n markdown = response.data.markdown;\n } catch (error: any) {\n logger?.debug('getTranscript:error', { error });\n throw new Error(`Failed to scrape YouTube page: ${error.message || String(error)}`);\n }\n\n // Parsing logic\n let transcriptContent = '';\n\n const transcriptStartValues = markdown.split('## Transcript');\n if (transcriptStartValues.length >= 2) {\n transcriptContent = transcriptStartValues[1];\n } else {\n // Fallback: looked for \"Show transcript\" button text which often precedes the transcript\n const showTranscriptSplit = markdown.split('Show transcript');\n if (showTranscriptSplit.length >= 2) {\n // Usually the transcript is after the last \"Show transcript\" occurrence\n transcriptContent = showTranscriptSplit[showTranscriptSplit.length - 1];\n logger?.debug('getTranscript:using-fallback-header', {\n data: { header: 'Show transcript' },\n });\n } else {\n // Last resort: try to parse the whole markdown, but this might pick up garbage\n logger?.warn('getTranscript:no-transcript-header-found', {\n data: { msg: 'Attempting to parse full markdown' },\n });\n transcriptContent = markdown;\n }\n }\n\n const segments: TranscriptItem[] = [];\n const lines = transcriptContent.split('\\n');\n let currentTimestamp = '';\n let currentText: string[] = [];\n\n const timestampRegex = /^(\\d{1,2}:)?\\d{1,2}:\\d{2}$/; // Matches 0:01, 10:05, 1:00:00\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n // Check if it's a timestamp\n if (timestampRegex.test(trimmed)) {\n // If we have a previous segment accumulating, push it\n if (currentTimestamp) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n currentText = [];\n }\n currentTimestamp = trimmed;\n } else {\n // It's text or a header?\n if (trimmed.startsWith('##')) continue;\n\n // Stop if we hit video thumbnails (footer)\n if (trimmed.startsWith('[![](')) {\n // Only break if we already found some transcript segments,\n // to avoid breaking on channel icon at the top if we parsed full markdown\n if (segments.length > 0) {\n break;\n }\n }\n\n // Stop if we hit language options (English/German) which usually signify end of transcript\n // Or \"Auto-dubbed\" etc.\n if (trimmed === 'English' || trimmed === 'German' || trimmed === 'Auto-dubbed') {\n continue;\n }\n\n if (currentTimestamp) {\n currentText.push(trimmed);\n }\n }\n }\n\n // Push last segment\n if (currentTimestamp && currentText.length > 0) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n }\n\n if (segments.length === 0) {\n throw new Error('Transcript parsing failed: No segments found after parsing.');\n }\n\n logger?.debug('getTranscript:success', { data: { count: segments.length } });\n return segments;\n };\n","import { Context } from '../types';\nimport { TranscriptItem } from './get-transcript';\n\nexport interface GetTranscriptHtmlInput {\n videoId: string;\n lang?: string;\n}\n\nexport interface GetTranscriptHtmlOutput {\n html: string;\n segments: TranscriptItem[];\n}\n\n// =============================================================================\n// InnerTube API Configuration\n// =============================================================================\n\nconst INNERTUBE = {\n // Public API key embedded in YouTube's JavaScript - may change over time\n DEFAULT_API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',\n API_URL: 'https://www.youtube.com/youtubei/v1/player',\n CLIENT_VERSION: '2.20250222.10.00',\n};\n\n// Current API key (can be refreshed if default stops working)\nlet currentApiKey: string = INNERTUBE.DEFAULT_API_KEY;\n\n/**\n * Fetch fresh API key from YouTube's homepage\n */\nconst fetchFreshApiKey = async (logger?: any): Promise<string> => {\n logger?.debug('Fetching fresh API key from YouTube...');\n\n const response = await fetch('https://www.youtube.com', {\n headers: {\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch YouTube homepage: ${response.status}`);\n }\n\n const html = await response.text();\n\n // Try multiple patterns to find the API key\n const patterns = [\n /\"INNERTUBE_API_KEY\":\"([^\"]+)\"/,\n /innertubeApiKey\":\"([^\"]+)\"/,\n /api_key=([A-Za-z0-9_-]+)/,\n ];\n\n for (const pattern of patterns) {\n const match = html.match(pattern);\n if (match && match[1]) {\n logger?.debug(`Found new API key: ${match[1].slice(0, 10)}...`);\n return match[1];\n }\n }\n\n throw new Error('Could not find API key in YouTube page');\n};\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\nconst generateVisitorData = (): string => {\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';\n return Array.from({ length: 11 }, () =>\n chars.charAt(Math.floor(Math.random() * chars.length)),\n ).join('');\n};\n\nconst decodeHtmlEntities = (text: string): string => {\n return text\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n .replace(/&#(\\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)))\n .replace(/&#x([a-fA-F0-9]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));\n};\n\n// =============================================================================\n// Types\n// =============================================================================\n\ninterface SubtitleEntry {\n start: number;\n duration: number;\n text: string;\n}\n\ninterface CaptionTrack {\n baseUrl: string;\n vssId: string;\n languageCode: string;\n name: { simpleText: string };\n}\n\ninterface PlayerResponse {\n playabilityStatus?: { status: string };\n captions?: {\n playerCaptionsTracklistRenderer?: {\n captionTracks?: CaptionTrack[];\n };\n };\n}\n\n// =============================================================================\n// Core Extraction Logic\n// =============================================================================\n\nconst getPlayerResponse = async (videoId: string, apiKey: string): Promise<PlayerResponse> => {\n const visitorData = generateVisitorData();\n\n const response = await fetch(`${INNERTUBE.API_URL}?key=${apiKey}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n 'X-Youtube-Client-Version': INNERTUBE.CLIENT_VERSION,\n 'X-Youtube-Client-Name': '1',\n 'X-Goog-Visitor-Id': visitorData,\n Origin: 'https://www.youtube.com',\n Referer: 'https://www.youtube.com/',\n },\n body: JSON.stringify({\n context: {\n client: {\n hl: 'en',\n gl: 'US',\n clientName: 'WEB',\n clientVersion: INNERTUBE.CLIENT_VERSION,\n visitorData,\n },\n },\n videoId,\n racyCheckOk: true,\n contentCheckOk: true,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`API request failed: ${response.status}`);\n }\n\n return response.json();\n};\n\nconst fetchCaptionXml = async (baseUrl: string, videoId: string): Promise<string> => {\n const url = baseUrl.replace('&fmt=srv3', '');\n\n const response = await fetch(url, {\n headers: {\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n Referer: `https://www.youtube.com/watch?v=${videoId}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Caption fetch failed: ${response.status}`);\n }\n\n return response.text();\n};\n\nconst parseXmlCaptions = (xml: string): SubtitleEntry[] => {\n if (!xml.includes('<text')) return [];\n\n return xml\n .split('</text>')\n .filter((line) => line.includes('<text'))\n .map((line) => {\n const startMatch = /start=\"([\\d.]+)\"/.exec(line);\n const durMatch = /dur=\"([\\d.]+)\"/.exec(line);\n const textMatch = /<text[^>]*>(.+)$/s.exec(line);\n\n if (!startMatch || !durMatch || !textMatch) return null;\n\n const rawText = textMatch[1].replace(/<[^>]*>/g, '').trim();\n const text = decodeHtmlEntities(rawText);\n\n return {\n start: parseFloat(startMatch[1]),\n duration: parseFloat(durMatch[1]),\n text,\n };\n })\n .filter((e): e is SubtitleEntry => e !== null && e.text.length > 0);\n};\n\n// Formatter for timestamp\nconst formatTime = (seconds: number): string => {\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n // const ms = Math.floor((seconds % 1) * 1000);\n // Returning format like 0:01, 10:05, 1:00:00 to match previous format if possible or standard HH:MM:SS\n if (h > 0) {\n return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;\n }\n return `${m}:${String(s).padStart(2, '0')}`;\n};\n\n/**\n * Core extraction logic (single attempt)\n */\nconst tryExtractSubtitles = async (\n videoId: string,\n apiKey: string,\n logger?: any,\n): Promise<TranscriptItem[]> => {\n const playerData = await getPlayerResponse(videoId, apiKey);\n\n if (playerData.playabilityStatus?.status !== 'OK') {\n throw new Error(`Video not playable: ${playerData.playabilityStatus?.status}`);\n }\n\n const tracks = playerData.captions?.playerCaptionsTracklistRenderer?.captionTracks || [];\n\n if (tracks.length === 0) {\n throw new Error('No subtitles available for this video');\n }\n\n // Default to first track (usually English or auto-generated English)\n // Could enhance to filter by lang if input.lang is provided\n const track = tracks[0];\n logger?.debug(`Found caption track: ${track.name?.simpleText} (${track.languageCode})`);\n\n const xml = await fetchCaptionXml(track.baseUrl, videoId);\n const subtitles = parseXmlCaptions(xml);\n\n if (subtitles.length === 0) {\n throw new Error('Failed to parse subtitles');\n }\n\n return subtitles.map((s) => ({\n timestamp: formatTime(s.start),\n text: s.text,\n }));\n};\n\nexport const getTranscriptHtml =\n (context: Context) =>\n async (input: GetTranscriptHtmlInput): Promise<GetTranscriptHtmlOutput> => {\n const { logger } = context;\n logger?.debug('getTranscriptHtml:start', { data: input });\n\n const videoId = input.videoId;\n const MAX_ATTEMPTS = 3;\n let lastError: Error | null = null;\n let segments: TranscriptItem[] = [];\n\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n try {\n logger?.debug(\n `Attempt ${attempt}/${MAX_ATTEMPTS} (API key: ${currentApiKey.slice(0, 10)}...)`,\n );\n\n segments = await tryExtractSubtitles(videoId, currentApiKey, logger);\n break; // Success\n } catch (error: any) {\n lastError = error instanceof Error ? error : new Error(String(error));\n logger?.warn(`Attempt ${attempt} failed: ${lastError.message}`);\n\n // If not last attempt, try to get fresh API key\n if (attempt < MAX_ATTEMPTS) {\n try {\n currentApiKey = await fetchFreshApiKey(logger);\n logger?.debug('Retrying with new API key...');\n } catch (keyError) {\n logger?.warn(`Failed to fetch new API key: ${keyError}`);\n }\n }\n }\n }\n\n if (segments.length === 0) {\n throw new Error(\n `Failed after ${MAX_ATTEMPTS} attempts. Last error: ${lastError?.message}`,\n );\n }\n\n // Generate a simple HTML representation for debugging/completeness\n const htmlParts = segments.map(\n (s) =>\n `<div class=\"segment\"><span class=\"timestamp\">${s.timestamp}</span><span class=\"text\">${s.text}</span></div>`,\n );\n const html = `<html><body><div class=\"transcript\">${htmlParts.join('\\n')}</div></body></html>`;\n\n logger?.debug('getTranscriptHtml:success', {\n data: { count: segments.length },\n });\n\n return {\n html,\n segments,\n };\n };\n","import { createClient, YoutubeClient } from './client';\nimport { Context, Logger } from './types';\nimport { search, SearchInput, SearchOutput } from './operations/search';\nimport { videoDetails, VideoDetailsInput, VideoDetailsOutput } from './operations/video-details';\nimport {\n getAllChannelVideos,\n GetAllChannelVideosInput,\n GetAllChannelVideosOutput,\n} from './operations/get-all-channel-videos';\nimport {\n getTranscript,\n GetTranscriptInput,\n GetTranscriptOutput,\n} from './operations/get-transcript';\nimport {\n getTranscriptHtml,\n GetTranscriptHtmlInput,\n GetTranscriptHtmlOutput,\n} from './operations/get-transcript-html';\n\nexport interface AdapterConfig {\n apiKey: string;\n firecrawlApiKey?: string;\n logger?: Logger;\n}\n\nexport interface Adapter {\n client: YoutubeClient;\n search: (input: SearchInput) => Promise<SearchOutput>;\n videoDetails: (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;\n getAllChannelVideos: (input: GetAllChannelVideosInput) => Promise<GetAllChannelVideosOutput>;\n getTranscript: (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;\n getTranscriptHtml: (input: GetTranscriptHtmlInput) => Promise<GetTranscriptHtmlOutput>;\n}\n\nimport { createAdapter as createFirecrawlAdapter, FirecrawlPlan } from '@vitkuz/firecrawl-adapter';\n\nexport const createAdapter = (config: AdapterConfig): Adapter => {\n const client = createClient(config.apiKey);\n\n let firecrawlAdapter;\n if (config.firecrawlApiKey) {\n firecrawlAdapter = createFirecrawlAdapter({\n apiKey: config.firecrawlApiKey,\n plan: FirecrawlPlan.FREE, // Default to free, could be configurable\n logger: config.logger,\n });\n }\n\n const context: Context = {\n client,\n firecrawlAdapter,\n logger: config.logger,\n };\n\n return {\n client,\n search: search(context),\n videoDetails: videoDetails(context),\n getAllChannelVideos: getAllChannelVideos(context),\n getTranscript: getTranscript(context),\n getTranscriptHtml: getTranscriptHtml(context),\n };\n};\n"]}
package/dist/index.mjs CHANGED
@@ -115,7 +115,11 @@ var getTranscript = (context) => async (input) => {
115
115
  const response = await firecrawlAdapter.scrape({
116
116
  url,
117
117
  params: {
118
- formats: ["markdown"]
118
+ formats: ["markdown"],
119
+ // Attempt to bypass blocks
120
+ waitFor: 5e3
121
+ // Try to request stealth proxy if supported by the plan/SDK
122
+ // Note: 'proxy' param availability depends on Firecrawl version/plan
119
123
  }
120
124
  });
121
125
  if (!response.success || !response.data || !response.data.markdown) {
@@ -128,19 +132,25 @@ var getTranscript = (context) => async (input) => {
128
132
  logger?.debug("getTranscript:error", { error });
129
133
  throw new Error(`Failed to scrape YouTube page: ${error.message || String(error)}`);
130
134
  }
131
- const segments = [];
135
+ let transcriptContent = "";
132
136
  const transcriptStartValues = markdown.split("## Transcript");
133
- if (transcriptStartValues.length < 2) {
134
- if (markdown.length < 500) {
135
- logger?.debug("getTranscript:markdown-too-short", { data: { markdown } });
137
+ if (transcriptStartValues.length >= 2) {
138
+ transcriptContent = transcriptStartValues[1];
139
+ } else {
140
+ const showTranscriptSplit = markdown.split("Show transcript");
141
+ if (showTranscriptSplit.length >= 2) {
142
+ transcriptContent = showTranscriptSplit[showTranscriptSplit.length - 1];
143
+ logger?.debug("getTranscript:using-fallback-header", {
144
+ data: { header: "Show transcript" }
145
+ });
136
146
  } else {
137
- logger?.debug("getTranscript:no-transcript-found", {
138
- data: { preview: markdown.slice(0, 1e3) }
147
+ logger?.warn("getTranscript:no-transcript-header-found", {
148
+ data: { msg: "Attempting to parse full markdown" }
139
149
  });
150
+ transcriptContent = markdown;
140
151
  }
141
- throw new Error("Transcript section not found in scraped content");
142
152
  }
143
- const transcriptContent = transcriptStartValues[1];
153
+ const segments = [];
144
154
  const lines = transcriptContent.split("\n");
145
155
  let currentTimestamp = "";
146
156
  let currentText = [];
@@ -160,9 +170,11 @@ var getTranscript = (context) => async (input) => {
160
170
  } else {
161
171
  if (trimmed.startsWith("##")) continue;
162
172
  if (trimmed.startsWith("[![](")) {
163
- break;
173
+ if (segments.length > 0) {
174
+ break;
175
+ }
164
176
  }
165
- if (trimmed === "English" || trimmed === "German") {
177
+ if (trimmed === "English" || trimmed === "German" || trimmed === "Auto-dubbed") {
166
178
  continue;
167
179
  }
168
180
  if (currentTimestamp) {
@@ -176,9 +188,192 @@ var getTranscript = (context) => async (input) => {
176
188
  text: currentText.join(" ").trim()
177
189
  });
178
190
  }
191
+ if (segments.length === 0) {
192
+ throw new Error("Transcript parsing failed: No segments found after parsing.");
193
+ }
179
194
  logger?.debug("getTranscript:success", { data: { count: segments.length } });
180
195
  return segments;
181
196
  };
197
+
198
+ // src/operations/get-transcript-html.ts
199
+ var INNERTUBE = {
200
+ // Public API key embedded in YouTube's JavaScript - may change over time
201
+ DEFAULT_API_KEY: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
202
+ API_URL: "https://www.youtube.com/youtubei/v1/player",
203
+ CLIENT_VERSION: "2.20250222.10.00"
204
+ };
205
+ var currentApiKey = INNERTUBE.DEFAULT_API_KEY;
206
+ var fetchFreshApiKey = async (logger) => {
207
+ logger?.debug("Fetching fresh API key from YouTube...");
208
+ const response = await fetch("https://www.youtube.com", {
209
+ headers: {
210
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
211
+ }
212
+ });
213
+ if (!response.ok) {
214
+ throw new Error(`Failed to fetch YouTube homepage: ${response.status}`);
215
+ }
216
+ const html = await response.text();
217
+ const patterns = [
218
+ /"INNERTUBE_API_KEY":"([^"]+)"/,
219
+ /innertubeApiKey":"([^"]+)"/,
220
+ /api_key=([A-Za-z0-9_-]+)/
221
+ ];
222
+ for (const pattern of patterns) {
223
+ const match = html.match(pattern);
224
+ if (match && match[1]) {
225
+ logger?.debug(`Found new API key: ${match[1].slice(0, 10)}...`);
226
+ return match[1];
227
+ }
228
+ }
229
+ throw new Error("Could not find API key in YouTube page");
230
+ };
231
+ var generateVisitorData = () => {
232
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
233
+ return Array.from(
234
+ { length: 11 },
235
+ () => chars.charAt(Math.floor(Math.random() * chars.length))
236
+ ).join("");
237
+ };
238
+ var decodeHtmlEntities = (text) => {
239
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&apos;/g, "'").replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10))).replace(/&#x([a-fA-F0-9]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
240
+ };
241
+ var getPlayerResponse = async (videoId, apiKey) => {
242
+ const visitorData = generateVisitorData();
243
+ const response = await fetch(`${INNERTUBE.API_URL}?key=${apiKey}`, {
244
+ method: "POST",
245
+ headers: {
246
+ "Content-Type": "application/json",
247
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
248
+ "X-Youtube-Client-Version": INNERTUBE.CLIENT_VERSION,
249
+ "X-Youtube-Client-Name": "1",
250
+ "X-Goog-Visitor-Id": visitorData,
251
+ Origin: "https://www.youtube.com",
252
+ Referer: "https://www.youtube.com/"
253
+ },
254
+ body: JSON.stringify({
255
+ context: {
256
+ client: {
257
+ hl: "en",
258
+ gl: "US",
259
+ clientName: "WEB",
260
+ clientVersion: INNERTUBE.CLIENT_VERSION,
261
+ visitorData
262
+ }
263
+ },
264
+ videoId,
265
+ racyCheckOk: true,
266
+ contentCheckOk: true
267
+ })
268
+ });
269
+ if (!response.ok) {
270
+ throw new Error(`API request failed: ${response.status}`);
271
+ }
272
+ return response.json();
273
+ };
274
+ var fetchCaptionXml = async (baseUrl, videoId) => {
275
+ const url = baseUrl.replace("&fmt=srv3", "");
276
+ const response = await fetch(url, {
277
+ headers: {
278
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
279
+ Referer: `https://www.youtube.com/watch?v=${videoId}`
280
+ }
281
+ });
282
+ if (!response.ok) {
283
+ throw new Error(`Caption fetch failed: ${response.status}`);
284
+ }
285
+ return response.text();
286
+ };
287
+ var parseXmlCaptions = (xml) => {
288
+ if (!xml.includes("<text")) return [];
289
+ return xml.split("</text>").filter((line) => line.includes("<text")).map((line) => {
290
+ const startMatch = /start="([\d.]+)"/.exec(line);
291
+ const durMatch = /dur="([\d.]+)"/.exec(line);
292
+ const textMatch = /<text[^>]*>(.+)$/s.exec(line);
293
+ if (!startMatch || !durMatch || !textMatch) return null;
294
+ const rawText = textMatch[1].replace(/<[^>]*>/g, "").trim();
295
+ const text = decodeHtmlEntities(rawText);
296
+ return {
297
+ start: parseFloat(startMatch[1]),
298
+ duration: parseFloat(durMatch[1]),
299
+ text
300
+ };
301
+ }).filter((e) => e !== null && e.text.length > 0);
302
+ };
303
+ var formatTime = (seconds) => {
304
+ const h = Math.floor(seconds / 3600);
305
+ const m = Math.floor(seconds % 3600 / 60);
306
+ const s = Math.floor(seconds % 60);
307
+ if (h > 0) {
308
+ return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
309
+ }
310
+ return `${m}:${String(s).padStart(2, "0")}`;
311
+ };
312
+ var tryExtractSubtitles = async (videoId, apiKey, logger) => {
313
+ const playerData = await getPlayerResponse(videoId, apiKey);
314
+ if (playerData.playabilityStatus?.status !== "OK") {
315
+ throw new Error(`Video not playable: ${playerData.playabilityStatus?.status}`);
316
+ }
317
+ const tracks = playerData.captions?.playerCaptionsTracklistRenderer?.captionTracks || [];
318
+ if (tracks.length === 0) {
319
+ throw new Error("No subtitles available for this video");
320
+ }
321
+ const track = tracks[0];
322
+ logger?.debug(`Found caption track: ${track.name?.simpleText} (${track.languageCode})`);
323
+ const xml = await fetchCaptionXml(track.baseUrl, videoId);
324
+ const subtitles = parseXmlCaptions(xml);
325
+ if (subtitles.length === 0) {
326
+ throw new Error("Failed to parse subtitles");
327
+ }
328
+ return subtitles.map((s) => ({
329
+ timestamp: formatTime(s.start),
330
+ text: s.text
331
+ }));
332
+ };
333
+ var getTranscriptHtml = (context) => async (input) => {
334
+ const { logger } = context;
335
+ logger?.debug("getTranscriptHtml:start", { data: input });
336
+ const videoId = input.videoId;
337
+ const MAX_ATTEMPTS = 3;
338
+ let lastError = null;
339
+ let segments = [];
340
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
341
+ try {
342
+ logger?.debug(
343
+ `Attempt ${attempt}/${MAX_ATTEMPTS} (API key: ${currentApiKey.slice(0, 10)}...)`
344
+ );
345
+ segments = await tryExtractSubtitles(videoId, currentApiKey, logger);
346
+ break;
347
+ } catch (error) {
348
+ lastError = error instanceof Error ? error : new Error(String(error));
349
+ logger?.warn(`Attempt ${attempt} failed: ${lastError.message}`);
350
+ if (attempt < MAX_ATTEMPTS) {
351
+ try {
352
+ currentApiKey = await fetchFreshApiKey(logger);
353
+ logger?.debug("Retrying with new API key...");
354
+ } catch (keyError) {
355
+ logger?.warn(`Failed to fetch new API key: ${keyError}`);
356
+ }
357
+ }
358
+ }
359
+ }
360
+ if (segments.length === 0) {
361
+ throw new Error(
362
+ `Failed after ${MAX_ATTEMPTS} attempts. Last error: ${lastError?.message}`
363
+ );
364
+ }
365
+ const htmlParts = segments.map(
366
+ (s) => `<div class="segment"><span class="timestamp">${s.timestamp}</span><span class="text">${s.text}</span></div>`
367
+ );
368
+ const html = `<html><body><div class="transcript">${htmlParts.join("\n")}</div></body></html>`;
369
+ logger?.debug("getTranscriptHtml:success", {
370
+ data: { count: segments.length }
371
+ });
372
+ return {
373
+ html,
374
+ segments
375
+ };
376
+ };
182
377
  var createAdapter = (config) => {
183
378
  const client = createClient(config.apiKey);
184
379
  let firecrawlAdapter;
@@ -200,10 +395,11 @@ var createAdapter = (config) => {
200
395
  search: search(context),
201
396
  videoDetails: videoDetails(context),
202
397
  getAllChannelVideos: getAllChannelVideos(context),
203
- getTranscript: getTranscript(context)
398
+ getTranscript: getTranscript(context),
399
+ getTranscriptHtml: getTranscriptHtml(context)
204
400
  };
205
401
  };
206
402
 
207
- export { createAdapter, createClient, getTranscript, search, videoDetails };
403
+ export { createAdapter, createClient, getTranscript, getTranscriptHtml, search, videoDetails };
208
404
  //# sourceMappingURL=index.mjs.map
209
405
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/operations/search.ts","../src/operations/video-details.ts","../src/operations/get-all-channel-videos.ts","../src/operations/get-transcript.ts","../src/adapter.ts"],"names":["createFirecrawlAdapter"],"mappings":"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","file":"index.mjs","sourcesContent":["import { google, youtube_v3 } from 'googleapis';\n\nexport type YoutubeClient = youtube_v3.Youtube;\n\nexport const createClient = (apiKey: string): YoutubeClient => {\n return google.youtube({\n version: 'v3',\n auth: apiKey,\n });\n};\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type SearchInput = youtube_v3.Params$Resource$Search$List;\nexport type SearchOutput = youtube_v3.Schema$SearchListResponse;\n\nexport const search =\n (context: Context) =>\n async (input: SearchInput): Promise<SearchOutput> => {\n const { client, logger } = context;\n\n logger?.debug('search:start', { data: input });\n\n try {\n const response = await client.search.list({\n part: ['snippet'],\n ...input,\n });\n\n logger?.debug('search:success');\n return response.data;\n } catch (error) {\n logger?.debug('search:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type VideoDetailsInput = youtube_v3.Params$Resource$Videos$List;\nexport type VideoDetailsOutput = youtube_v3.Schema$VideoListResponse;\n\nexport const videoDetails =\n (context: Context) =>\n async (input: VideoDetailsInput): Promise<VideoDetailsOutput> => {\n const { client, logger } = context;\n\n logger?.debug('videoDetails:start', { data: input });\n\n try {\n const response = await client.videos.list({\n part: ['snippet', 'contentDetails', 'statistics'],\n ...input,\n });\n\n logger?.debug('videoDetails:success');\n return response.data;\n } catch (error) {\n logger?.debug('videoDetails:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport interface GetAllChannelVideosInput {\n channelId: string;\n}\n\nexport interface GetAllChannelVideosOutput {\n items: youtube_v3.Schema$Video[];\n totalCount: number;\n}\n\nexport const getAllChannelVideos =\n (context: Context) =>\n async (input: GetAllChannelVideosInput): Promise<GetAllChannelVideosOutput> => {\n const { client, logger } = context;\n\n logger?.debug('getAllChannelVideos:start', { data: input });\n\n try {\n // Step 1: Get the Uploads playlist ID\n const channelResponse = await client.channels.list({\n id: [input.channelId],\n part: ['contentDetails'],\n });\n\n const uploadsPlaylistId =\n channelResponse.data.items?.[0]?.contentDetails?.relatedPlaylists?.uploads;\n\n if (!uploadsPlaylistId) {\n throw new Error(\n `Could not find uploads playlist for channel ID: ${input.channelId}`,\n );\n }\n\n logger?.debug('getAllChannelVideos:found_playlist', { data: { uploadsPlaylistId } });\n\n // Step 2: Recursively fetch all playlist items and their video details\n let items: youtube_v3.Schema$Video[] = [];\n let nextPageToken: string | undefined = undefined;\n\n do {\n // Get playlist items (Video IDs)\n const playlistResponse: { data: youtube_v3.Schema$PlaylistItemListResponse } =\n (await client.playlistItems.list({\n playlistId: uploadsPlaylistId,\n part: ['contentDetails'],\n maxResults: 50,\n pageToken: nextPageToken,\n })) as any;\n\n const playlistItems = playlistResponse.data.items || [];\n\n if (playlistItems.length > 0) {\n const videoIds = playlistItems\n .map((item) => item.contentDetails?.videoId)\n .filter((id): id is string => !!id);\n\n if (videoIds.length > 0) {\n // Fetch full video details\n const videosResponse = await client.videos.list({\n id: videoIds,\n part: ['snippet', 'contentDetails', 'statistics'],\n });\n\n const videoItems = videosResponse.data.items || [];\n items = items.concat(videoItems);\n }\n }\n\n nextPageToken = playlistResponse.data.nextPageToken || undefined;\n\n logger?.debug('getAllChannelVideos:fetched_page', {\n data: {\n fetched: playlistItems.length,\n totalVideosSoFar: items.length,\n hasNextPage: !!nextPageToken,\n },\n });\n } while (nextPageToken);\n\n logger?.debug('getAllChannelVideos:success', { data: { totalCount: items.length } });\n\n return {\n items,\n totalCount: items.length,\n };\n } catch (error) {\n logger?.debug('getAllChannelVideos:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\n\nexport interface GetTranscriptInput {\n videoId: string;\n lang?: string;\n}\n\nexport interface TranscriptItem {\n timestamp: string;\n text: string;\n}\n\nexport type GetTranscriptOutput = TranscriptItem[];\n\nexport const getTranscript =\n (context: Context) =>\n async (input: GetTranscriptInput): Promise<GetTranscriptOutput> => {\n const { logger, firecrawlAdapter } = context;\n logger?.debug('getTranscript:start', { data: input });\n\n if (!firecrawlAdapter) {\n throw new Error(\n 'Firecrawl adapter is not initialized. Provide firecrawlApiKey in config.',\n );\n }\n\n const url = `https://www.youtube.com/watch?v=${input.videoId}`;\n let markdown: string;\n\n try {\n // Use firecrawlAdapter.scrape directly\n const response = await firecrawlAdapter.scrape({\n url,\n params: {\n formats: ['markdown'],\n },\n });\n\n if (!response.success || !response.data || !response.data.markdown) {\n throw new Error(\n `Failed to scrape YouTube page: ${response.error || 'No data returned'}`,\n );\n }\n markdown = response.data.markdown;\n } catch (error: any) {\n logger?.debug('getTranscript:error', { error });\n throw new Error(`Failed to scrape YouTube page: ${error.message || String(error)}`);\n }\n\n // Parsing logic reused from firecrawl adapter implementation\n const segments: TranscriptItem[] = [];\n\n const transcriptStartValues = markdown.split('## Transcript');\n if (transcriptStartValues.length < 2) {\n if (markdown.length < 500) {\n logger?.debug('getTranscript:markdown-too-short', { data: { markdown } });\n } else {\n logger?.debug('getTranscript:no-transcript-found', {\n data: { preview: markdown.slice(0, 1000) },\n });\n }\n throw new Error('Transcript section not found in scraped content');\n }\n\n const transcriptContent = transcriptStartValues[1];\n\n const lines = transcriptContent.split('\\n');\n let currentTimestamp = '';\n let currentText: string[] = [];\n\n const timestampRegex = /^(\\d{1,2}:)?\\d{1,2}:\\d{2}$/; // Matches 0:01, 10:05, 1:00:00\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n // Check if it's a timestamp\n if (timestampRegex.test(trimmed)) {\n // If we have a previous segment accumulating, push it\n if (currentTimestamp) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n currentText = [];\n }\n currentTimestamp = trimmed;\n } else {\n // It's text or a header?\n if (trimmed.startsWith('##')) continue;\n\n // Stop if we hit video thumbnails (footer)\n if (trimmed.startsWith('[![](')) {\n break;\n }\n\n // Stop if we hit language options (English/German) which usually signify end of transcript\n if (trimmed === 'English' || trimmed === 'German') {\n continue;\n }\n\n if (currentTimestamp) {\n currentText.push(trimmed);\n }\n }\n }\n\n // Push last segment\n if (currentTimestamp && currentText.length > 0) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n }\n\n logger?.debug('getTranscript:success', { data: { count: segments.length } });\n return segments;\n };\n","import { createClient, YoutubeClient } from './client';\nimport { Context, Logger } from './types';\nimport { search, SearchInput, SearchOutput } from './operations/search';\nimport { videoDetails, VideoDetailsInput, VideoDetailsOutput } from './operations/video-details';\nimport {\n getAllChannelVideos,\n GetAllChannelVideosInput,\n GetAllChannelVideosOutput,\n} from './operations/get-all-channel-videos';\nimport {\n getTranscript,\n GetTranscriptInput,\n GetTranscriptOutput,\n} from './operations/get-transcript';\n\nexport interface AdapterConfig {\n apiKey: string;\n firecrawlApiKey?: string;\n logger?: Logger;\n}\n\nexport interface Adapter {\n client: YoutubeClient;\n search: (input: SearchInput) => Promise<SearchOutput>;\n videoDetails: (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;\n getAllChannelVideos: (input: GetAllChannelVideosInput) => Promise<GetAllChannelVideosOutput>;\n getTranscript: (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;\n}\n\nimport { createAdapter as createFirecrawlAdapter, FirecrawlPlan } from '@vitkuz/firecrawl-adapter';\n\nexport const createAdapter = (config: AdapterConfig): Adapter => {\n const client = createClient(config.apiKey);\n\n let firecrawlAdapter;\n if (config.firecrawlApiKey) {\n firecrawlAdapter = createFirecrawlAdapter({\n apiKey: config.firecrawlApiKey,\n plan: FirecrawlPlan.FREE, // Default to free, could be configurable\n logger: config.logger,\n });\n }\n\n const context: Context = {\n client,\n firecrawlAdapter,\n logger: config.logger,\n };\n\n return {\n client,\n search: search(context),\n videoDetails: videoDetails(context),\n getAllChannelVideos: getAllChannelVideos(context),\n getTranscript: getTranscript(context),\n };\n};\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/operations/search.ts","../src/operations/video-details.ts","../src/operations/get-all-channel-videos.ts","../src/operations/get-transcript.ts","../src/operations/get-transcript-html.ts","../src/adapter.ts"],"names":["createFirecrawlAdapter"],"mappings":"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","file":"index.mjs","sourcesContent":["import { google, youtube_v3 } from 'googleapis';\n\nexport type YoutubeClient = youtube_v3.Youtube;\n\nexport const createClient = (apiKey: string): YoutubeClient => {\n return google.youtube({\n version: 'v3',\n auth: apiKey,\n });\n};\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type SearchInput = youtube_v3.Params$Resource$Search$List;\nexport type SearchOutput = youtube_v3.Schema$SearchListResponse;\n\nexport const search =\n (context: Context) =>\n async (input: SearchInput): Promise<SearchOutput> => {\n const { client, logger } = context;\n\n logger?.debug('search:start', { data: input });\n\n try {\n const response = await client.search.list({\n part: ['snippet'],\n ...input,\n });\n\n logger?.debug('search:success');\n return response.data;\n } catch (error) {\n logger?.debug('search:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport type VideoDetailsInput = youtube_v3.Params$Resource$Videos$List;\nexport type VideoDetailsOutput = youtube_v3.Schema$VideoListResponse;\n\nexport const videoDetails =\n (context: Context) =>\n async (input: VideoDetailsInput): Promise<VideoDetailsOutput> => {\n const { client, logger } = context;\n\n logger?.debug('videoDetails:start', { data: input });\n\n try {\n const response = await client.videos.list({\n part: ['snippet', 'contentDetails', 'statistics'],\n ...input,\n });\n\n logger?.debug('videoDetails:success');\n return response.data;\n } catch (error) {\n logger?.debug('videoDetails:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\nimport { youtube_v3 } from 'googleapis';\n\nexport interface GetAllChannelVideosInput {\n channelId: string;\n}\n\nexport interface GetAllChannelVideosOutput {\n items: youtube_v3.Schema$Video[];\n totalCount: number;\n}\n\nexport const getAllChannelVideos =\n (context: Context) =>\n async (input: GetAllChannelVideosInput): Promise<GetAllChannelVideosOutput> => {\n const { client, logger } = context;\n\n logger?.debug('getAllChannelVideos:start', { data: input });\n\n try {\n // Step 1: Get the Uploads playlist ID\n const channelResponse = await client.channels.list({\n id: [input.channelId],\n part: ['contentDetails'],\n });\n\n const uploadsPlaylistId =\n channelResponse.data.items?.[0]?.contentDetails?.relatedPlaylists?.uploads;\n\n if (!uploadsPlaylistId) {\n throw new Error(\n `Could not find uploads playlist for channel ID: ${input.channelId}`,\n );\n }\n\n logger?.debug('getAllChannelVideos:found_playlist', { data: { uploadsPlaylistId } });\n\n // Step 2: Recursively fetch all playlist items and their video details\n let items: youtube_v3.Schema$Video[] = [];\n let nextPageToken: string | undefined = undefined;\n\n do {\n // Get playlist items (Video IDs)\n const playlistResponse: { data: youtube_v3.Schema$PlaylistItemListResponse } =\n (await client.playlistItems.list({\n playlistId: uploadsPlaylistId,\n part: ['contentDetails'],\n maxResults: 50,\n pageToken: nextPageToken,\n })) as any;\n\n const playlistItems = playlistResponse.data.items || [];\n\n if (playlistItems.length > 0) {\n const videoIds = playlistItems\n .map((item) => item.contentDetails?.videoId)\n .filter((id): id is string => !!id);\n\n if (videoIds.length > 0) {\n // Fetch full video details\n const videosResponse = await client.videos.list({\n id: videoIds,\n part: ['snippet', 'contentDetails', 'statistics'],\n });\n\n const videoItems = videosResponse.data.items || [];\n items = items.concat(videoItems);\n }\n }\n\n nextPageToken = playlistResponse.data.nextPageToken || undefined;\n\n logger?.debug('getAllChannelVideos:fetched_page', {\n data: {\n fetched: playlistItems.length,\n totalVideosSoFar: items.length,\n hasNextPage: !!nextPageToken,\n },\n });\n } while (nextPageToken);\n\n logger?.debug('getAllChannelVideos:success', { data: { totalCount: items.length } });\n\n return {\n items,\n totalCount: items.length,\n };\n } catch (error) {\n logger?.debug('getAllChannelVideos:error', { error });\n throw error;\n }\n };\n","import { Context } from '../types';\n\nexport interface GetTranscriptInput {\n videoId: string;\n lang?: string;\n}\n\nexport interface TranscriptItem {\n timestamp: string;\n text: string;\n}\n\nexport type GetTranscriptOutput = TranscriptItem[];\n\nexport const getTranscript =\n (context: Context) =>\n async (input: GetTranscriptInput): Promise<GetTranscriptOutput> => {\n const { logger, firecrawlAdapter } = context;\n logger?.debug('getTranscript:start', { data: input });\n\n if (!firecrawlAdapter) {\n throw new Error(\n 'Firecrawl adapter is not initialized. Provide firecrawlApiKey in config.',\n );\n }\n\n const url = `https://www.youtube.com/watch?v=${input.videoId}`;\n let markdown: string;\n\n try {\n // Use firecrawlAdapter.scrape directly\n const response = await firecrawlAdapter.scrape({\n url,\n params: {\n formats: ['markdown'],\n // Attempt to bypass blocks\n waitFor: 5000,\n // Try to request stealth proxy if supported by the plan/SDK\n // Note: 'proxy' param availability depends on Firecrawl version/plan\n },\n });\n\n if (!response.success || !response.data || !response.data.markdown) {\n // If 403 or other error, it might be in response.error or just failure\n throw new Error(\n `Failed to scrape YouTube page: ${response.error || 'No data returned'}`,\n );\n }\n markdown = response.data.markdown;\n } catch (error: any) {\n logger?.debug('getTranscript:error', { error });\n throw new Error(`Failed to scrape YouTube page: ${error.message || String(error)}`);\n }\n\n // Parsing logic\n let transcriptContent = '';\n\n const transcriptStartValues = markdown.split('## Transcript');\n if (transcriptStartValues.length >= 2) {\n transcriptContent = transcriptStartValues[1];\n } else {\n // Fallback: looked for \"Show transcript\" button text which often precedes the transcript\n const showTranscriptSplit = markdown.split('Show transcript');\n if (showTranscriptSplit.length >= 2) {\n // Usually the transcript is after the last \"Show transcript\" occurrence\n transcriptContent = showTranscriptSplit[showTranscriptSplit.length - 1];\n logger?.debug('getTranscript:using-fallback-header', {\n data: { header: 'Show transcript' },\n });\n } else {\n // Last resort: try to parse the whole markdown, but this might pick up garbage\n logger?.warn('getTranscript:no-transcript-header-found', {\n data: { msg: 'Attempting to parse full markdown' },\n });\n transcriptContent = markdown;\n }\n }\n\n const segments: TranscriptItem[] = [];\n const lines = transcriptContent.split('\\n');\n let currentTimestamp = '';\n let currentText: string[] = [];\n\n const timestampRegex = /^(\\d{1,2}:)?\\d{1,2}:\\d{2}$/; // Matches 0:01, 10:05, 1:00:00\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n // Check if it's a timestamp\n if (timestampRegex.test(trimmed)) {\n // If we have a previous segment accumulating, push it\n if (currentTimestamp) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n currentText = [];\n }\n currentTimestamp = trimmed;\n } else {\n // It's text or a header?\n if (trimmed.startsWith('##')) continue;\n\n // Stop if we hit video thumbnails (footer)\n if (trimmed.startsWith('[![](')) {\n // Only break if we already found some transcript segments,\n // to avoid breaking on channel icon at the top if we parsed full markdown\n if (segments.length > 0) {\n break;\n }\n }\n\n // Stop if we hit language options (English/German) which usually signify end of transcript\n // Or \"Auto-dubbed\" etc.\n if (trimmed === 'English' || trimmed === 'German' || trimmed === 'Auto-dubbed') {\n continue;\n }\n\n if (currentTimestamp) {\n currentText.push(trimmed);\n }\n }\n }\n\n // Push last segment\n if (currentTimestamp && currentText.length > 0) {\n segments.push({\n timestamp: currentTimestamp,\n text: currentText.join(' ').trim(),\n });\n }\n\n if (segments.length === 0) {\n throw new Error('Transcript parsing failed: No segments found after parsing.');\n }\n\n logger?.debug('getTranscript:success', { data: { count: segments.length } });\n return segments;\n };\n","import { Context } from '../types';\nimport { TranscriptItem } from './get-transcript';\n\nexport interface GetTranscriptHtmlInput {\n videoId: string;\n lang?: string;\n}\n\nexport interface GetTranscriptHtmlOutput {\n html: string;\n segments: TranscriptItem[];\n}\n\n// =============================================================================\n// InnerTube API Configuration\n// =============================================================================\n\nconst INNERTUBE = {\n // Public API key embedded in YouTube's JavaScript - may change over time\n DEFAULT_API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',\n API_URL: 'https://www.youtube.com/youtubei/v1/player',\n CLIENT_VERSION: '2.20250222.10.00',\n};\n\n// Current API key (can be refreshed if default stops working)\nlet currentApiKey: string = INNERTUBE.DEFAULT_API_KEY;\n\n/**\n * Fetch fresh API key from YouTube's homepage\n */\nconst fetchFreshApiKey = async (logger?: any): Promise<string> => {\n logger?.debug('Fetching fresh API key from YouTube...');\n\n const response = await fetch('https://www.youtube.com', {\n headers: {\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch YouTube homepage: ${response.status}`);\n }\n\n const html = await response.text();\n\n // Try multiple patterns to find the API key\n const patterns = [\n /\"INNERTUBE_API_KEY\":\"([^\"]+)\"/,\n /innertubeApiKey\":\"([^\"]+)\"/,\n /api_key=([A-Za-z0-9_-]+)/,\n ];\n\n for (const pattern of patterns) {\n const match = html.match(pattern);\n if (match && match[1]) {\n logger?.debug(`Found new API key: ${match[1].slice(0, 10)}...`);\n return match[1];\n }\n }\n\n throw new Error('Could not find API key in YouTube page');\n};\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\nconst generateVisitorData = (): string => {\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';\n return Array.from({ length: 11 }, () =>\n chars.charAt(Math.floor(Math.random() * chars.length)),\n ).join('');\n};\n\nconst decodeHtmlEntities = (text: string): string => {\n return text\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n .replace(/&#(\\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)))\n .replace(/&#x([a-fA-F0-9]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));\n};\n\n// =============================================================================\n// Types\n// =============================================================================\n\ninterface SubtitleEntry {\n start: number;\n duration: number;\n text: string;\n}\n\ninterface CaptionTrack {\n baseUrl: string;\n vssId: string;\n languageCode: string;\n name: { simpleText: string };\n}\n\ninterface PlayerResponse {\n playabilityStatus?: { status: string };\n captions?: {\n playerCaptionsTracklistRenderer?: {\n captionTracks?: CaptionTrack[];\n };\n };\n}\n\n// =============================================================================\n// Core Extraction Logic\n// =============================================================================\n\nconst getPlayerResponse = async (videoId: string, apiKey: string): Promise<PlayerResponse> => {\n const visitorData = generateVisitorData();\n\n const response = await fetch(`${INNERTUBE.API_URL}?key=${apiKey}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n 'X-Youtube-Client-Version': INNERTUBE.CLIENT_VERSION,\n 'X-Youtube-Client-Name': '1',\n 'X-Goog-Visitor-Id': visitorData,\n Origin: 'https://www.youtube.com',\n Referer: 'https://www.youtube.com/',\n },\n body: JSON.stringify({\n context: {\n client: {\n hl: 'en',\n gl: 'US',\n clientName: 'WEB',\n clientVersion: INNERTUBE.CLIENT_VERSION,\n visitorData,\n },\n },\n videoId,\n racyCheckOk: true,\n contentCheckOk: true,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`API request failed: ${response.status}`);\n }\n\n return response.json();\n};\n\nconst fetchCaptionXml = async (baseUrl: string, videoId: string): Promise<string> => {\n const url = baseUrl.replace('&fmt=srv3', '');\n\n const response = await fetch(url, {\n headers: {\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n Referer: `https://www.youtube.com/watch?v=${videoId}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(`Caption fetch failed: ${response.status}`);\n }\n\n return response.text();\n};\n\nconst parseXmlCaptions = (xml: string): SubtitleEntry[] => {\n if (!xml.includes('<text')) return [];\n\n return xml\n .split('</text>')\n .filter((line) => line.includes('<text'))\n .map((line) => {\n const startMatch = /start=\"([\\d.]+)\"/.exec(line);\n const durMatch = /dur=\"([\\d.]+)\"/.exec(line);\n const textMatch = /<text[^>]*>(.+)$/s.exec(line);\n\n if (!startMatch || !durMatch || !textMatch) return null;\n\n const rawText = textMatch[1].replace(/<[^>]*>/g, '').trim();\n const text = decodeHtmlEntities(rawText);\n\n return {\n start: parseFloat(startMatch[1]),\n duration: parseFloat(durMatch[1]),\n text,\n };\n })\n .filter((e): e is SubtitleEntry => e !== null && e.text.length > 0);\n};\n\n// Formatter for timestamp\nconst formatTime = (seconds: number): string => {\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = Math.floor(seconds % 60);\n // const ms = Math.floor((seconds % 1) * 1000);\n // Returning format like 0:01, 10:05, 1:00:00 to match previous format if possible or standard HH:MM:SS\n if (h > 0) {\n return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;\n }\n return `${m}:${String(s).padStart(2, '0')}`;\n};\n\n/**\n * Core extraction logic (single attempt)\n */\nconst tryExtractSubtitles = async (\n videoId: string,\n apiKey: string,\n logger?: any,\n): Promise<TranscriptItem[]> => {\n const playerData = await getPlayerResponse(videoId, apiKey);\n\n if (playerData.playabilityStatus?.status !== 'OK') {\n throw new Error(`Video not playable: ${playerData.playabilityStatus?.status}`);\n }\n\n const tracks = playerData.captions?.playerCaptionsTracklistRenderer?.captionTracks || [];\n\n if (tracks.length === 0) {\n throw new Error('No subtitles available for this video');\n }\n\n // Default to first track (usually English or auto-generated English)\n // Could enhance to filter by lang if input.lang is provided\n const track = tracks[0];\n logger?.debug(`Found caption track: ${track.name?.simpleText} (${track.languageCode})`);\n\n const xml = await fetchCaptionXml(track.baseUrl, videoId);\n const subtitles = parseXmlCaptions(xml);\n\n if (subtitles.length === 0) {\n throw new Error('Failed to parse subtitles');\n }\n\n return subtitles.map((s) => ({\n timestamp: formatTime(s.start),\n text: s.text,\n }));\n};\n\nexport const getTranscriptHtml =\n (context: Context) =>\n async (input: GetTranscriptHtmlInput): Promise<GetTranscriptHtmlOutput> => {\n const { logger } = context;\n logger?.debug('getTranscriptHtml:start', { data: input });\n\n const videoId = input.videoId;\n const MAX_ATTEMPTS = 3;\n let lastError: Error | null = null;\n let segments: TranscriptItem[] = [];\n\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n try {\n logger?.debug(\n `Attempt ${attempt}/${MAX_ATTEMPTS} (API key: ${currentApiKey.slice(0, 10)}...)`,\n );\n\n segments = await tryExtractSubtitles(videoId, currentApiKey, logger);\n break; // Success\n } catch (error: any) {\n lastError = error instanceof Error ? error : new Error(String(error));\n logger?.warn(`Attempt ${attempt} failed: ${lastError.message}`);\n\n // If not last attempt, try to get fresh API key\n if (attempt < MAX_ATTEMPTS) {\n try {\n currentApiKey = await fetchFreshApiKey(logger);\n logger?.debug('Retrying with new API key...');\n } catch (keyError) {\n logger?.warn(`Failed to fetch new API key: ${keyError}`);\n }\n }\n }\n }\n\n if (segments.length === 0) {\n throw new Error(\n `Failed after ${MAX_ATTEMPTS} attempts. Last error: ${lastError?.message}`,\n );\n }\n\n // Generate a simple HTML representation for debugging/completeness\n const htmlParts = segments.map(\n (s) =>\n `<div class=\"segment\"><span class=\"timestamp\">${s.timestamp}</span><span class=\"text\">${s.text}</span></div>`,\n );\n const html = `<html><body><div class=\"transcript\">${htmlParts.join('\\n')}</div></body></html>`;\n\n logger?.debug('getTranscriptHtml:success', {\n data: { count: segments.length },\n });\n\n return {\n html,\n segments,\n };\n };\n","import { createClient, YoutubeClient } from './client';\nimport { Context, Logger } from './types';\nimport { search, SearchInput, SearchOutput } from './operations/search';\nimport { videoDetails, VideoDetailsInput, VideoDetailsOutput } from './operations/video-details';\nimport {\n getAllChannelVideos,\n GetAllChannelVideosInput,\n GetAllChannelVideosOutput,\n} from './operations/get-all-channel-videos';\nimport {\n getTranscript,\n GetTranscriptInput,\n GetTranscriptOutput,\n} from './operations/get-transcript';\nimport {\n getTranscriptHtml,\n GetTranscriptHtmlInput,\n GetTranscriptHtmlOutput,\n} from './operations/get-transcript-html';\n\nexport interface AdapterConfig {\n apiKey: string;\n firecrawlApiKey?: string;\n logger?: Logger;\n}\n\nexport interface Adapter {\n client: YoutubeClient;\n search: (input: SearchInput) => Promise<SearchOutput>;\n videoDetails: (input: VideoDetailsInput) => Promise<VideoDetailsOutput>;\n getAllChannelVideos: (input: GetAllChannelVideosInput) => Promise<GetAllChannelVideosOutput>;\n getTranscript: (input: GetTranscriptInput) => Promise<GetTranscriptOutput>;\n getTranscriptHtml: (input: GetTranscriptHtmlInput) => Promise<GetTranscriptHtmlOutput>;\n}\n\nimport { createAdapter as createFirecrawlAdapter, FirecrawlPlan } from '@vitkuz/firecrawl-adapter';\n\nexport const createAdapter = (config: AdapterConfig): Adapter => {\n const client = createClient(config.apiKey);\n\n let firecrawlAdapter;\n if (config.firecrawlApiKey) {\n firecrawlAdapter = createFirecrawlAdapter({\n apiKey: config.firecrawlApiKey,\n plan: FirecrawlPlan.FREE, // Default to free, could be configurable\n logger: config.logger,\n });\n }\n\n const context: Context = {\n client,\n firecrawlAdapter,\n logger: config.logger,\n };\n\n return {\n client,\n search: search(context),\n videoDetails: videoDetails(context),\n getAllChannelVideos: getAllChannelVideos(context),\n getTranscript: getTranscript(context),\n getTranscriptHtml: getTranscriptHtml(context),\n };\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitkuz/youtube-adapter",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Functional YouTube adapter",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -23,7 +23,7 @@
23
23
  "prepublishOnly": "npm run format && npm run build"
24
24
  },
25
25
  "dependencies": {
26
- "@vitkuz/firecrawl-adapter": "file:../vitkuz-firecrawl-adapter",
26
+ "@vitkuz/firecrawl-adapter": "^1.1.0",
27
27
  "dotenv": "^16.4.5",
28
28
  "googleapis": "^144.0.0"
29
29
  },
@@ -34,5 +34,14 @@
34
34
  "tsup": "^8.0.0",
35
35
  "tsx": "^4.21.0",
36
36
  "typescript": "^5.0.0"
37
- }
37
+ },
38
+ "author": "Vitali Kuzmenka",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/vitkuz/vitkuz-youtube-adapter.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/vitkuz/vitkuz-youtube-adapter/issues"
45
+ },
46
+ "homepage": "https://github.com/vitkuz/vitkuz-youtube-adapter#readme"
38
47
  }