@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 +12 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +209 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +209 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -3
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
|
-
|
|
137
|
+
let transcriptContent = "";
|
|
134
138
|
const transcriptStartValues = markdown.split("## Transcript");
|
|
135
|
-
if (transcriptStartValues.length
|
|
136
|
-
|
|
137
|
-
|
|
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?.
|
|
140
|
-
data: {
|
|
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
|
|
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
|
-
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/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(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/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
|
-
|
|
135
|
+
let transcriptContent = "";
|
|
132
136
|
const transcriptStartValues = markdown.split("## Transcript");
|
|
133
|
-
if (transcriptStartValues.length
|
|
134
|
-
|
|
135
|
-
|
|
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?.
|
|
138
|
-
data: {
|
|
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
|
|
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
|
-
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/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
|
package/dist/index.mjs.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":["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(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/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.
|
|
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": "
|
|
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
|
}
|