@rv404/mcp-launchpad 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.env.example +29 -0
  2. package/README.md +248 -0
  3. package/dist/backends/clickup.d.ts +3 -0
  4. package/dist/backends/clickup.d.ts.map +1 -0
  5. package/dist/backends/clickup.js +1550 -0
  6. package/dist/backends/clickup.js.map +1 -0
  7. package/dist/backends/email.d.ts +9 -0
  8. package/dist/backends/email.d.ts.map +1 -0
  9. package/dist/backends/email.js +452 -0
  10. package/dist/backends/email.js.map +1 -0
  11. package/dist/backends/firecrawl.d.ts +3 -0
  12. package/dist/backends/firecrawl.d.ts.map +1 -0
  13. package/dist/backends/firecrawl.js +297 -0
  14. package/dist/backends/firecrawl.js.map +1 -0
  15. package/dist/backends/gemini.d.ts +3 -0
  16. package/dist/backends/gemini.d.ts.map +1 -0
  17. package/dist/backends/gemini.js +702 -0
  18. package/dist/backends/gemini.js.map +1 -0
  19. package/dist/backends/github.d.ts +3 -0
  20. package/dist/backends/github.d.ts.map +1 -0
  21. package/dist/backends/github.js +2129 -0
  22. package/dist/backends/github.js.map +1 -0
  23. package/dist/backends/index.d.ts +2 -0
  24. package/dist/backends/index.d.ts.map +1 -0
  25. package/dist/backends/index.js +27 -0
  26. package/dist/backends/index.js.map +1 -0
  27. package/dist/backends/kie.d.ts +3 -0
  28. package/dist/backends/kie.d.ts.map +1 -0
  29. package/dist/backends/kie.js +399 -0
  30. package/dist/backends/kie.js.map +1 -0
  31. package/dist/backends/n8n.d.ts +3 -0
  32. package/dist/backends/n8n.d.ts.map +1 -0
  33. package/dist/backends/n8n.js +414 -0
  34. package/dist/backends/n8n.js.map +1 -0
  35. package/dist/backends/notion.d.ts +3 -0
  36. package/dist/backends/notion.d.ts.map +1 -0
  37. package/dist/backends/notion.js +375 -0
  38. package/dist/backends/notion.js.map +1 -0
  39. package/dist/backends/quickbooks.d.ts +3 -0
  40. package/dist/backends/quickbooks.d.ts.map +1 -0
  41. package/dist/backends/quickbooks.js +2183 -0
  42. package/dist/backends/quickbooks.js.map +1 -0
  43. package/dist/backends/slack.d.ts +3 -0
  44. package/dist/backends/slack.d.ts.map +1 -0
  45. package/dist/backends/slack.js +1725 -0
  46. package/dist/backends/slack.js.map +1 -0
  47. package/dist/backends/youtube.d.ts +3 -0
  48. package/dist/backends/youtube.d.ts.map +1 -0
  49. package/dist/backends/youtube.js +936 -0
  50. package/dist/backends/youtube.js.map +1 -0
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +150 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/router.d.ts +36 -0
  56. package/dist/router.d.ts.map +1 -0
  57. package/dist/router.js +283 -0
  58. package/dist/router.js.map +1 -0
  59. package/dist/types.d.ts +103 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +65 -0
  62. package/dist/types.js.map +1 -0
  63. package/lib/mcp-file-output/dist/index.d.ts +99 -0
  64. package/lib/mcp-file-output/dist/index.d.ts.map +1 -0
  65. package/lib/mcp-file-output/dist/index.js +215 -0
  66. package/lib/mcp-file-output/dist/index.js.map +1 -0
  67. package/lib/mcp-file-output/node_modules/@types/node/LICENSE +21 -0
  68. package/lib/mcp-file-output/node_modules/@types/node/README.md +15 -0
  69. package/lib/mcp-file-output/node_modules/@types/node/assert/strict.d.ts +8 -0
  70. package/lib/mcp-file-output/node_modules/@types/node/assert.d.ts +1062 -0
  71. package/lib/mcp-file-output/node_modules/@types/node/async_hooks.d.ts +605 -0
  72. package/lib/mcp-file-output/node_modules/@types/node/buffer.buffer.d.ts +471 -0
  73. package/lib/mcp-file-output/node_modules/@types/node/buffer.d.ts +1936 -0
  74. package/lib/mcp-file-output/node_modules/@types/node/child_process.d.ts +1475 -0
  75. package/lib/mcp-file-output/node_modules/@types/node/cluster.d.ts +577 -0
  76. package/lib/mcp-file-output/node_modules/@types/node/compatibility/disposable.d.ts +16 -0
  77. package/lib/mcp-file-output/node_modules/@types/node/compatibility/index.d.ts +9 -0
  78. package/lib/mcp-file-output/node_modules/@types/node/compatibility/indexable.d.ts +20 -0
  79. package/lib/mcp-file-output/node_modules/@types/node/compatibility/iterators.d.ts +21 -0
  80. package/lib/mcp-file-output/node_modules/@types/node/console.d.ts +452 -0
  81. package/lib/mcp-file-output/node_modules/@types/node/constants.d.ts +21 -0
  82. package/lib/mcp-file-output/node_modules/@types/node/crypto.d.ts +4590 -0
  83. package/lib/mcp-file-output/node_modules/@types/node/dgram.d.ts +597 -0
  84. package/lib/mcp-file-output/node_modules/@types/node/diagnostics_channel.d.ts +578 -0
  85. package/lib/mcp-file-output/node_modules/@types/node/dns/promises.d.ts +479 -0
  86. package/lib/mcp-file-output/node_modules/@types/node/dns.d.ts +871 -0
  87. package/lib/mcp-file-output/node_modules/@types/node/domain.d.ts +170 -0
  88. package/lib/mcp-file-output/node_modules/@types/node/events.d.ts +977 -0
  89. package/lib/mcp-file-output/node_modules/@types/node/fs/promises.d.ts +1270 -0
  90. package/lib/mcp-file-output/node_modules/@types/node/fs.d.ts +4375 -0
  91. package/lib/mcp-file-output/node_modules/@types/node/globals.d.ts +172 -0
  92. package/lib/mcp-file-output/node_modules/@types/node/globals.typedarray.d.ts +38 -0
  93. package/lib/mcp-file-output/node_modules/@types/node/http.d.ts +2049 -0
  94. package/lib/mcp-file-output/node_modules/@types/node/http2.d.ts +2631 -0
  95. package/lib/mcp-file-output/node_modules/@types/node/https.d.ts +578 -0
  96. package/lib/mcp-file-output/node_modules/@types/node/index.d.ts +93 -0
  97. package/lib/mcp-file-output/node_modules/@types/node/inspector.generated.d.ts +3966 -0
  98. package/lib/mcp-file-output/node_modules/@types/node/module.d.ts +539 -0
  99. package/lib/mcp-file-output/node_modules/@types/node/net.d.ts +1012 -0
  100. package/lib/mcp-file-output/node_modules/@types/node/os.d.ts +506 -0
  101. package/lib/mcp-file-output/node_modules/@types/node/package.json +140 -0
  102. package/lib/mcp-file-output/node_modules/@types/node/path.d.ts +200 -0
  103. package/lib/mcp-file-output/node_modules/@types/node/perf_hooks.d.ts +961 -0
  104. package/lib/mcp-file-output/node_modules/@types/node/process.d.ts +1957 -0
  105. package/lib/mcp-file-output/node_modules/@types/node/punycode.d.ts +117 -0
  106. package/lib/mcp-file-output/node_modules/@types/node/querystring.d.ts +152 -0
  107. package/lib/mcp-file-output/node_modules/@types/node/readline/promises.d.ts +162 -0
  108. package/lib/mcp-file-output/node_modules/@types/node/readline.d.ts +589 -0
  109. package/lib/mcp-file-output/node_modules/@types/node/repl.d.ts +430 -0
  110. package/lib/mcp-file-output/node_modules/@types/node/sea.d.ts +153 -0
  111. package/lib/mcp-file-output/node_modules/@types/node/stream/consumers.d.ts +38 -0
  112. package/lib/mcp-file-output/node_modules/@types/node/stream/promises.d.ts +90 -0
  113. package/lib/mcp-file-output/node_modules/@types/node/stream/web.d.ts +533 -0
  114. package/lib/mcp-file-output/node_modules/@types/node/stream.d.ts +1675 -0
  115. package/lib/mcp-file-output/node_modules/@types/node/string_decoder.d.ts +67 -0
  116. package/lib/mcp-file-output/node_modules/@types/node/test.d.ts +1787 -0
  117. package/lib/mcp-file-output/node_modules/@types/node/timers/promises.d.ts +108 -0
  118. package/lib/mcp-file-output/node_modules/@types/node/timers.d.ts +286 -0
  119. package/lib/mcp-file-output/node_modules/@types/node/tls.d.ts +1255 -0
  120. package/lib/mcp-file-output/node_modules/@types/node/trace_events.d.ts +197 -0
  121. package/lib/mcp-file-output/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +468 -0
  122. package/lib/mcp-file-output/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +34 -0
  123. package/lib/mcp-file-output/node_modules/@types/node/ts5.6/index.d.ts +93 -0
  124. package/lib/mcp-file-output/node_modules/@types/node/tty.d.ts +208 -0
  125. package/lib/mcp-file-output/node_modules/@types/node/url.d.ts +964 -0
  126. package/lib/mcp-file-output/node_modules/@types/node/util.d.ts +2331 -0
  127. package/lib/mcp-file-output/node_modules/@types/node/v8.d.ts +809 -0
  128. package/lib/mcp-file-output/node_modules/@types/node/vm.d.ts +1001 -0
  129. package/lib/mcp-file-output/node_modules/@types/node/wasi.d.ts +181 -0
  130. package/lib/mcp-file-output/node_modules/@types/node/web-globals/abortcontroller.d.ts +34 -0
  131. package/lib/mcp-file-output/node_modules/@types/node/web-globals/domexception.d.ts +68 -0
  132. package/lib/mcp-file-output/node_modules/@types/node/web-globals/events.d.ts +97 -0
  133. package/lib/mcp-file-output/node_modules/@types/node/web-globals/fetch.d.ts +46 -0
  134. package/lib/mcp-file-output/node_modules/@types/node/worker_threads.d.ts +715 -0
  135. package/lib/mcp-file-output/node_modules/@types/node/zlib.d.ts +540 -0
  136. package/lib/mcp-file-output/package.json +19 -0
  137. package/lib/mcp-file-output/src/index.ts +309 -0
  138. package/lib/mcp-file-output/tsconfig.json +20 -0
  139. package/package.json +64 -0
@@ -0,0 +1,936 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
5
+ const execAsync = promisify(exec);
6
+ // ============================================================================
7
+ // Configuration
8
+ // ============================================================================
9
+ // YouTube Data API v3 (optional - for metadata)
10
+ const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY;
11
+ // Gemini API for video analysis (reuses existing env var)
12
+ const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY;
13
+ const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';
14
+ // Output directory for downloads - uses env var or user home directory
15
+ const getDefaultDataDir = () => {
16
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
17
+ return path.join(homeDir, '.router-mcp', 'data');
18
+ };
19
+ const DATA_DIR = process.env.ROUTER_MCP_DATA_DIR || process.env.MCP_FILE_OUTPUT_DIR || getDefaultDataDir();
20
+ const YOUTUBE_DIR = path.join(DATA_DIR, 'youtube');
21
+ const DOWNLOADS_DIR = path.join(YOUTUBE_DIR, 'downloads');
22
+ const TRANSCRIPTS_DIR = path.join(YOUTUBE_DIR, 'transcripts');
23
+ const ANALYSES_DIR = path.join(YOUTUBE_DIR, 'analyses');
24
+ // Ensure directories exist
25
+ function ensureDirectories() {
26
+ for (const dir of [YOUTUBE_DIR, DOWNLOADS_DIR, TRANSCRIPTS_DIR, ANALYSES_DIR]) {
27
+ if (!fs.existsSync(dir)) {
28
+ fs.mkdirSync(dir, { recursive: true });
29
+ }
30
+ }
31
+ }
32
+ // ============================================================================
33
+ // Action Definitions
34
+ // ============================================================================
35
+ const actions = [
36
+ // Metadata
37
+ {
38
+ name: 'get_metadata',
39
+ description: 'Get video metadata: title, channel, duration, description, view count, publish date, thumbnails.',
40
+ params: [
41
+ { name: 'url', type: 'string', required: true, description: 'YouTube video URL or video ID' },
42
+ ],
43
+ },
44
+ // Transcript
45
+ {
46
+ name: 'get_transcript',
47
+ description: 'Get video transcript/captions with timestamps. Supports multiple languages.',
48
+ params: [
49
+ { name: 'url', type: 'string', required: true, description: 'YouTube video URL or video ID' },
50
+ { name: 'language', type: 'string', required: false, description: 'Language code (e.g., "en", "es", "auto"). Default: "en"' },
51
+ { name: 'format', type: 'string', required: false, description: 'Output format: "text" (plain text), "timestamped" (with timestamps), "srt" (SubRip format). Default: "timestamped"' },
52
+ ],
53
+ },
54
+ // Chapters
55
+ {
56
+ name: 'get_chapters',
57
+ description: 'Get video chapters if available (from description timestamps).',
58
+ params: [
59
+ { name: 'url', type: 'string', required: true, description: 'YouTube video URL or video ID' },
60
+ ],
61
+ },
62
+ // Downloads
63
+ {
64
+ name: 'download_audio',
65
+ description: 'Download audio from a YouTube video (MP3 format). Requires yt-dlp installed.',
66
+ params: [
67
+ { name: 'url', type: 'string', required: true, description: 'YouTube video URL or video ID' },
68
+ { name: 'outputPath', type: 'string', required: false, description: 'Custom output path. Default: auto-generated in youtube/downloads/' },
69
+ ],
70
+ },
71
+ {
72
+ name: 'download_video',
73
+ description: 'Download video from YouTube. Requires yt-dlp installed.',
74
+ params: [
75
+ { name: 'url', type: 'string', required: true, description: 'YouTube video URL or video ID' },
76
+ { name: 'quality', type: 'string', required: false, description: 'Video quality: "best", "1080p", "720p", "480p", "worst". Default: "720p"' },
77
+ { name: 'outputPath', type: 'string', required: false, description: 'Custom output path. Default: auto-generated in youtube/downloads/' },
78
+ ],
79
+ },
80
+ // Analysis
81
+ {
82
+ name: 'summarize',
83
+ description: 'Get a concise summary of the video based on its transcript.',
84
+ params: [
85
+ { name: 'url', type: 'string', required: true, description: 'YouTube video URL or video ID' },
86
+ { name: 'style', type: 'string', required: false, description: 'Summary style: "brief" (1-2 paragraphs), "detailed" (section by section), "bullets" (key points). Default: "brief"' },
87
+ ],
88
+ },
89
+ {
90
+ name: 'analyze',
91
+ description: 'Full hybrid analysis: transcript analysis + optional Gemini visual analysis for comprehensive understanding.',
92
+ params: [
93
+ { name: 'url', type: 'string', required: true, description: 'YouTube video URL or video ID' },
94
+ { name: 'mode', type: 'string', required: false, description: 'Analysis mode: "transcript" (fast, text-only), "visual" (Gemini video analysis), "full" (both combined). Default: "transcript"' },
95
+ { name: 'questions', type: 'array', required: false, description: 'Specific questions to answer about the video content' },
96
+ { name: 'extractCode', type: 'boolean', required: false, description: 'Extract code snippets from programming videos. Default: false' },
97
+ { name: 'outputPath', type: 'string', required: false, description: 'Save analysis to file. Default: returns in response' },
98
+ ],
99
+ },
100
+ {
101
+ name: 'extract_code',
102
+ description: 'Extract code snippets mentioned or shown in programming tutorial videos.',
103
+ params: [
104
+ { name: 'url', type: 'string', required: true, description: 'YouTube video URL or video ID' },
105
+ { name: 'language', type: 'string', required: false, description: 'Programming language to focus on (e.g., "python", "javascript"). Default: auto-detect' },
106
+ ],
107
+ },
108
+ ];
109
+ // ============================================================================
110
+ // Utility Functions
111
+ // ============================================================================
112
+ function extractVideoId(urlOrId) {
113
+ // Already a video ID (11 characters)
114
+ if (/^[a-zA-Z0-9_-]{11}$/.test(urlOrId)) {
115
+ return urlOrId;
116
+ }
117
+ // Various YouTube URL formats
118
+ const patterns = [
119
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/)([a-zA-Z0-9_-]{11})/,
120
+ /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/,
121
+ ];
122
+ for (const pattern of patterns) {
123
+ const match = urlOrId.match(pattern);
124
+ if (match) {
125
+ return match[1];
126
+ }
127
+ }
128
+ throw new Error(`Invalid YouTube URL or video ID: ${urlOrId}`);
129
+ }
130
+ function formatDuration(seconds) {
131
+ const hours = Math.floor(seconds / 3600);
132
+ const minutes = Math.floor((seconds % 3600) / 60);
133
+ const secs = Math.floor(seconds % 60);
134
+ if (hours > 0) {
135
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
136
+ }
137
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
138
+ }
139
+ function formatTimestamp(seconds) {
140
+ const hours = Math.floor(seconds / 3600);
141
+ const minutes = Math.floor((seconds % 3600) / 60);
142
+ const secs = Math.floor(seconds % 60);
143
+ const ms = Math.floor((seconds % 1) * 1000);
144
+ if (hours > 0) {
145
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`;
146
+ }
147
+ return `00:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`;
148
+ }
149
+ // yt-dlp executable - should be installed and available on PATH
150
+ // Install with: pip install yt-dlp
151
+ const YT_DLP_PATHS = [
152
+ 'yt-dlp', // Standard: on PATH (recommended)
153
+ 'yt-dlp.exe', // Windows: on PATH
154
+ ];
155
+ let ytDlpPath = null;
156
+ async function checkYtDlp() {
157
+ // Return cached path if already found
158
+ if (ytDlpPath)
159
+ return true;
160
+ for (const ytdlp of YT_DLP_PATHS) {
161
+ try {
162
+ await execAsync(`"${ytdlp}" --version`);
163
+ ytDlpPath = ytdlp;
164
+ return true;
165
+ }
166
+ catch {
167
+ continue;
168
+ }
169
+ }
170
+ return false;
171
+ }
172
+ function getYtDlpCommand() {
173
+ return ytDlpPath ? `"${ytDlpPath}"` : 'yt-dlp';
174
+ }
175
+ // ============================================================================
176
+ // Get Metadata (using yt-dlp for reliability)
177
+ // ============================================================================
178
+ async function getMetadata(params) {
179
+ const { url } = params;
180
+ if (!url || typeof url !== 'string') {
181
+ throw new Error('url is required');
182
+ }
183
+ const videoId = extractVideoId(url);
184
+ // Try YouTube Data API first if available
185
+ if (YOUTUBE_API_KEY) {
186
+ try {
187
+ const apiUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&id=${videoId}&key=${YOUTUBE_API_KEY}`;
188
+ const response = await fetch(apiUrl);
189
+ if (response.ok) {
190
+ const data = await response.json();
191
+ if (data.items && data.items.length > 0) {
192
+ const item = data.items[0];
193
+ const snippet = item.snippet || {};
194
+ const contentDetails = item.contentDetails || {};
195
+ const statistics = item.statistics || {};
196
+ // Parse ISO 8601 duration
197
+ let durationSeconds = 0;
198
+ const durationMatch = contentDetails.duration?.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
199
+ if (durationMatch) {
200
+ durationSeconds = (parseInt(durationMatch[1] || '0') * 3600) +
201
+ (parseInt(durationMatch[2] || '0') * 60) +
202
+ parseInt(durationMatch[3] || '0');
203
+ }
204
+ return {
205
+ success: true,
206
+ videoId,
207
+ title: snippet.title,
208
+ channel: snippet.channelTitle,
209
+ description: snippet.description,
210
+ publishedAt: snippet.publishedAt,
211
+ duration: formatDuration(durationSeconds),
212
+ durationSeconds,
213
+ viewCount: statistics.viewCount ? parseInt(statistics.viewCount) : null,
214
+ likeCount: statistics.likeCount ? parseInt(statistics.likeCount) : null,
215
+ commentCount: statistics.commentCount ? parseInt(statistics.commentCount) : null,
216
+ tags: snippet.tags || [],
217
+ thumbnails: snippet.thumbnails,
218
+ url: `https://www.youtube.com/watch?v=${videoId}`,
219
+ };
220
+ }
221
+ }
222
+ }
223
+ catch {
224
+ // Fall through to yt-dlp
225
+ }
226
+ }
227
+ // Fallback to yt-dlp
228
+ if (!(await checkYtDlp())) {
229
+ throw new Error('yt-dlp is not installed. Install it with: pip install yt-dlp');
230
+ }
231
+ try {
232
+ const { stdout } = await execAsync(`${getYtDlpCommand()} --dump-json --no-download "https://www.youtube.com/watch?v=${videoId}"`, { maxBuffer: 10 * 1024 * 1024 });
233
+ const info = JSON.parse(stdout);
234
+ return {
235
+ success: true,
236
+ videoId,
237
+ title: info.title,
238
+ channel: info.channel || info.uploader,
239
+ description: info.description,
240
+ publishedAt: info.upload_date ? `${info.upload_date.slice(0, 4)}-${info.upload_date.slice(4, 6)}-${info.upload_date.slice(6, 8)}` : null,
241
+ duration: info.duration ? formatDuration(info.duration) : null,
242
+ durationSeconds: info.duration,
243
+ viewCount: info.view_count,
244
+ likeCount: info.like_count,
245
+ commentCount: info.comment_count,
246
+ tags: info.tags || [],
247
+ thumbnail: info.thumbnail,
248
+ hasChapters: !!(info.chapters && info.chapters.length > 0),
249
+ chapterCount: info.chapters?.length || 0,
250
+ url: `https://www.youtube.com/watch?v=${videoId}`,
251
+ };
252
+ }
253
+ catch (error) {
254
+ throw new Error(`Failed to get metadata: ${error instanceof Error ? error.message : String(error)}`);
255
+ }
256
+ }
257
+ // ============================================================================
258
+ // Get Transcript
259
+ // ============================================================================
260
+ async function getTranscript(params) {
261
+ const { url, language = 'en', format = 'timestamped' } = params;
262
+ if (!url || typeof url !== 'string') {
263
+ throw new Error('url is required');
264
+ }
265
+ const videoId = extractVideoId(url);
266
+ if (!(await checkYtDlp())) {
267
+ throw new Error('yt-dlp is not installed. Install it with: pip install yt-dlp');
268
+ }
269
+ ensureDirectories();
270
+ // Download subtitles using yt-dlp
271
+ const tempDir = path.join(TRANSCRIPTS_DIR, `temp_${videoId}_${Date.now()}`);
272
+ fs.mkdirSync(tempDir, { recursive: true });
273
+ try {
274
+ // Try to get subtitles (auto-generated or manual)
275
+ const langArg = language === 'auto' ? '--sub-langs "en.*,.*"' : `--sub-langs "${language}.*,en.*"`;
276
+ await execAsync(`${getYtDlpCommand()} --write-auto-sub --write-sub ${langArg} --sub-format json3 --skip-download -o "${path.join(tempDir, '%(id)s')}" "https://www.youtube.com/watch?v=${videoId}"`, { maxBuffer: 10 * 1024 * 1024 });
277
+ // Find the downloaded subtitle file
278
+ const files = fs.readdirSync(tempDir);
279
+ const subFile = files.find(f => f.endsWith('.json3'));
280
+ if (!subFile) {
281
+ // Try VTT format as fallback
282
+ await execAsync(`${getYtDlpCommand()} --write-auto-sub --write-sub ${langArg} --sub-format vtt --skip-download -o "${path.join(tempDir, '%(id)s')}" "https://www.youtube.com/watch?v=${videoId}"`, { maxBuffer: 10 * 1024 * 1024 });
283
+ const vttFiles = fs.readdirSync(tempDir);
284
+ const vttFile = vttFiles.find(f => f.endsWith('.vtt'));
285
+ if (!vttFile) {
286
+ throw new Error('No captions available for this video');
287
+ }
288
+ // Parse VTT file
289
+ const vttContent = fs.readFileSync(path.join(tempDir, vttFile), 'utf-8');
290
+ const segments = parseVTT(vttContent);
291
+ return formatTranscriptOutput(videoId, segments, format);
292
+ }
293
+ // Parse JSON3 format
294
+ const json3Content = fs.readFileSync(path.join(tempDir, subFile), 'utf-8');
295
+ const json3Data = JSON.parse(json3Content);
296
+ const segments = [];
297
+ if (json3Data.events) {
298
+ for (const event of json3Data.events) {
299
+ if (event.segs && event.tStartMs !== undefined) {
300
+ const text = event.segs.map(s => s.utf8 || '').join('').trim();
301
+ if (text && !text.startsWith('[')) { // Skip [Music] etc
302
+ segments.push({
303
+ start: event.tStartMs / 1000,
304
+ duration: (event.dDurationMs || 0) / 1000,
305
+ text,
306
+ });
307
+ }
308
+ }
309
+ }
310
+ }
311
+ return formatTranscriptOutput(videoId, segments, format);
312
+ }
313
+ finally {
314
+ // Cleanup temp directory
315
+ try {
316
+ fs.rmSync(tempDir, { recursive: true, force: true });
317
+ }
318
+ catch {
319
+ // Ignore cleanup errors
320
+ }
321
+ }
322
+ }
323
+ function parseVTT(content) {
324
+ const segments = [];
325
+ const lines = content.split('\n');
326
+ let i = 0;
327
+ while (i < lines.length) {
328
+ const line = lines[i].trim();
329
+ // Look for timestamp line (00:00:00.000 --> 00:00:00.000)
330
+ const timestampMatch = line.match(/(\d{2}:\d{2}:\d{2}\.\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}\.\d{3})/);
331
+ if (timestampMatch) {
332
+ const startTime = parseVTTTimestamp(timestampMatch[1]);
333
+ const endTime = parseVTTTimestamp(timestampMatch[2]);
334
+ // Collect text lines until empty line
335
+ const textLines = [];
336
+ i++;
337
+ while (i < lines.length && lines[i].trim() !== '') {
338
+ // Remove VTT tags like <c> </c>
339
+ const cleanedLine = lines[i].replace(/<[^>]+>/g, '').trim();
340
+ if (cleanedLine && !cleanedLine.startsWith('[')) {
341
+ textLines.push(cleanedLine);
342
+ }
343
+ i++;
344
+ }
345
+ if (textLines.length > 0) {
346
+ segments.push({
347
+ start: startTime,
348
+ duration: endTime - startTime,
349
+ text: textLines.join(' '),
350
+ });
351
+ }
352
+ }
353
+ i++;
354
+ }
355
+ return segments;
356
+ }
357
+ function parseVTTTimestamp(timestamp) {
358
+ const parts = timestamp.split(':');
359
+ const hours = parseInt(parts[0]);
360
+ const minutes = parseInt(parts[1]);
361
+ const secondsParts = parts[2].split('.');
362
+ const seconds = parseInt(secondsParts[0]);
363
+ const ms = parseInt(secondsParts[1] || '0');
364
+ return hours * 3600 + minutes * 60 + seconds + ms / 1000;
365
+ }
366
+ function formatTranscriptOutput(videoId, segments, format) {
367
+ // Deduplicate consecutive similar segments
368
+ const deduped = [];
369
+ for (const seg of segments) {
370
+ if (deduped.length === 0 || deduped[deduped.length - 1].text !== seg.text) {
371
+ deduped.push(seg);
372
+ }
373
+ }
374
+ if (format === 'text') {
375
+ return {
376
+ success: true,
377
+ videoId,
378
+ format: 'text',
379
+ text: deduped.map(s => s.text).join(' '),
380
+ segmentCount: deduped.length,
381
+ };
382
+ }
383
+ if (format === 'srt') {
384
+ const srtLines = [];
385
+ deduped.forEach((seg, idx) => {
386
+ srtLines.push(String(idx + 1));
387
+ srtLines.push(`${formatTimestamp(seg.start)} --> ${formatTimestamp(seg.start + seg.duration)}`);
388
+ srtLines.push(seg.text);
389
+ srtLines.push('');
390
+ });
391
+ return {
392
+ success: true,
393
+ videoId,
394
+ format: 'srt',
395
+ srt: srtLines.join('\n'),
396
+ segmentCount: deduped.length,
397
+ };
398
+ }
399
+ // Default: timestamped
400
+ return {
401
+ success: true,
402
+ videoId,
403
+ format: 'timestamped',
404
+ segments: deduped.map(s => ({
405
+ timestamp: formatDuration(s.start),
406
+ startSeconds: s.start,
407
+ text: s.text,
408
+ })),
409
+ segmentCount: deduped.length,
410
+ totalDuration: deduped.length > 0
411
+ ? formatDuration(deduped[deduped.length - 1].start + deduped[deduped.length - 1].duration)
412
+ : '0:00',
413
+ };
414
+ }
415
+ // ============================================================================
416
+ // Get Chapters
417
+ // ============================================================================
418
+ async function getChapters(params) {
419
+ const { url } = params;
420
+ if (!url || typeof url !== 'string') {
421
+ throw new Error('url is required');
422
+ }
423
+ const videoId = extractVideoId(url);
424
+ if (!(await checkYtDlp())) {
425
+ throw new Error('yt-dlp is not installed. Install it with: pip install yt-dlp');
426
+ }
427
+ try {
428
+ const { stdout } = await execAsync(`${getYtDlpCommand()} --dump-json --no-download "https://www.youtube.com/watch?v=${videoId}"`, { maxBuffer: 10 * 1024 * 1024 });
429
+ const info = JSON.parse(stdout);
430
+ if (!info.chapters || info.chapters.length === 0) {
431
+ return {
432
+ success: true,
433
+ videoId,
434
+ hasChapters: false,
435
+ chapters: [],
436
+ message: 'This video does not have chapters',
437
+ };
438
+ }
439
+ return {
440
+ success: true,
441
+ videoId,
442
+ title: info.title,
443
+ hasChapters: true,
444
+ chapterCount: info.chapters.length,
445
+ chapters: info.chapters.map((ch, idx) => ({
446
+ index: idx + 1,
447
+ title: ch.title,
448
+ startTime: formatDuration(ch.start_time),
449
+ startSeconds: ch.start_time,
450
+ endTime: formatDuration(ch.end_time),
451
+ endSeconds: ch.end_time,
452
+ duration: formatDuration(ch.end_time - ch.start_time),
453
+ })),
454
+ };
455
+ }
456
+ catch (error) {
457
+ throw new Error(`Failed to get chapters: ${error instanceof Error ? error.message : String(error)}`);
458
+ }
459
+ }
460
+ // ============================================================================
461
+ // Download Audio
462
+ // ============================================================================
463
+ async function downloadAudio(params) {
464
+ const { url, outputPath } = params;
465
+ if (!url || typeof url !== 'string') {
466
+ throw new Error('url is required');
467
+ }
468
+ const videoId = extractVideoId(url);
469
+ if (!(await checkYtDlp())) {
470
+ throw new Error('yt-dlp is not installed. Install it with: pip install yt-dlp');
471
+ }
472
+ ensureDirectories();
473
+ // Determine output path
474
+ let finalPath;
475
+ if (outputPath && typeof outputPath === 'string') {
476
+ finalPath = outputPath;
477
+ }
478
+ else {
479
+ // Get video title for filename
480
+ try {
481
+ const { stdout } = await execAsync(`${getYtDlpCommand()} --get-title "https://www.youtube.com/watch?v=${videoId}"`, { maxBuffer: 1024 * 1024 });
482
+ const title = stdout.trim().replace(/[<>:"/\\|?*]/g, '_').slice(0, 100);
483
+ finalPath = path.join(DOWNLOADS_DIR, `${title}_${videoId}.mp3`);
484
+ }
485
+ catch {
486
+ finalPath = path.join(DOWNLOADS_DIR, `${videoId}.mp3`);
487
+ }
488
+ }
489
+ // Ensure output directory exists
490
+ const dir = path.dirname(finalPath);
491
+ if (!fs.existsSync(dir)) {
492
+ fs.mkdirSync(dir, { recursive: true });
493
+ }
494
+ try {
495
+ await execAsync(`${getYtDlpCommand()} -x --audio-format mp3 --audio-quality 0 -o "${finalPath.replace('.mp3', '')}.%(ext)s" "https://www.youtube.com/watch?v=${videoId}"`, { maxBuffer: 10 * 1024 * 1024 });
496
+ // Check if file exists
497
+ if (!fs.existsSync(finalPath)) {
498
+ // Try with the exact output name
499
+ const altPath = finalPath.replace('.mp3', '.mp3');
500
+ if (fs.existsSync(altPath)) {
501
+ return {
502
+ success: true,
503
+ videoId,
504
+ format: 'mp3',
505
+ path: altPath,
506
+ size: fs.statSync(altPath).size,
507
+ };
508
+ }
509
+ // Look for any mp3 file in the directory with the video ID
510
+ const files = fs.readdirSync(dir);
511
+ const mp3File = files.find(f => f.includes(videoId) && f.endsWith('.mp3'));
512
+ if (mp3File) {
513
+ const foundPath = path.join(dir, mp3File);
514
+ return {
515
+ success: true,
516
+ videoId,
517
+ format: 'mp3',
518
+ path: foundPath,
519
+ size: fs.statSync(foundPath).size,
520
+ };
521
+ }
522
+ }
523
+ const stats = fs.statSync(finalPath);
524
+ return {
525
+ success: true,
526
+ videoId,
527
+ format: 'mp3',
528
+ path: finalPath,
529
+ size: stats.size,
530
+ sizeFormatted: `${(stats.size / (1024 * 1024)).toFixed(2)} MB`,
531
+ };
532
+ }
533
+ catch (error) {
534
+ throw new Error(`Failed to download audio: ${error instanceof Error ? error.message : String(error)}`);
535
+ }
536
+ }
537
+ // ============================================================================
538
+ // Download Video
539
+ // ============================================================================
540
+ async function downloadVideo(params) {
541
+ const { url, quality = '720p', outputPath } = params;
542
+ if (!url || typeof url !== 'string') {
543
+ throw new Error('url is required');
544
+ }
545
+ const videoId = extractVideoId(url);
546
+ if (!(await checkYtDlp())) {
547
+ throw new Error('yt-dlp is not installed. Install it with: pip install yt-dlp');
548
+ }
549
+ ensureDirectories();
550
+ // Quality mapping
551
+ const qualityMap = {
552
+ 'best': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
553
+ '1080p': 'bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]',
554
+ '720p': 'bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720][ext=mp4]',
555
+ '480p': 'bestvideo[height<=480][ext=mp4]+bestaudio[ext=m4a]/best[height<=480][ext=mp4]',
556
+ 'worst': 'worstvideo[ext=mp4]+worstaudio[ext=m4a]/worst[ext=mp4]/worst',
557
+ };
558
+ const formatStr = qualityMap[quality] || qualityMap['720p'];
559
+ // Determine output path
560
+ let finalPath;
561
+ if (outputPath && typeof outputPath === 'string') {
562
+ finalPath = outputPath;
563
+ }
564
+ else {
565
+ try {
566
+ const { stdout } = await execAsync(`${getYtDlpCommand()} --get-title "https://www.youtube.com/watch?v=${videoId}"`, { maxBuffer: 1024 * 1024 });
567
+ const title = stdout.trim().replace(/[<>:"/\\|?*]/g, '_').slice(0, 100);
568
+ finalPath = path.join(DOWNLOADS_DIR, `${title}_${videoId}.mp4`);
569
+ }
570
+ catch {
571
+ finalPath = path.join(DOWNLOADS_DIR, `${videoId}.mp4`);
572
+ }
573
+ }
574
+ // Ensure output directory exists
575
+ const dir = path.dirname(finalPath);
576
+ if (!fs.existsSync(dir)) {
577
+ fs.mkdirSync(dir, { recursive: true });
578
+ }
579
+ try {
580
+ await execAsync(`${getYtDlpCommand()} -f "${formatStr}" --merge-output-format mp4 -o "${finalPath}" "https://www.youtube.com/watch?v=${videoId}"`, { maxBuffer: 50 * 1024 * 1024, timeout: 300000 });
581
+ // Verify file exists
582
+ if (!fs.existsSync(finalPath)) {
583
+ // Look for any mp4 with the video ID
584
+ const files = fs.readdirSync(dir);
585
+ const mp4File = files.find(f => f.includes(videoId) && f.endsWith('.mp4'));
586
+ if (mp4File) {
587
+ finalPath = path.join(dir, mp4File);
588
+ }
589
+ else {
590
+ throw new Error('Download completed but file not found');
591
+ }
592
+ }
593
+ const stats = fs.statSync(finalPath);
594
+ return {
595
+ success: true,
596
+ videoId,
597
+ format: 'mp4',
598
+ quality,
599
+ path: finalPath,
600
+ size: stats.size,
601
+ sizeFormatted: `${(stats.size / (1024 * 1024)).toFixed(2)} MB`,
602
+ };
603
+ }
604
+ catch (error) {
605
+ throw new Error(`Failed to download video: ${error instanceof Error ? error.message : String(error)}`);
606
+ }
607
+ }
608
+ // ============================================================================
609
+ // Summarize
610
+ // ============================================================================
611
+ async function summarize(params) {
612
+ const { url, style = 'brief' } = params;
613
+ if (!url || typeof url !== 'string') {
614
+ throw new Error('url is required');
615
+ }
616
+ if (!GOOGLE_API_KEY) {
617
+ throw new Error('GOOGLE_API_KEY environment variable is not set (needed for summarization)');
618
+ }
619
+ const videoId = extractVideoId(url);
620
+ // Get transcript
621
+ const transcriptResult = await getTranscript({ url, format: 'text' });
622
+ if (!transcriptResult.success || !transcriptResult.text) {
623
+ throw new Error('Failed to get transcript for summarization');
624
+ }
625
+ // Get metadata for context
626
+ const metadataResult = await getMetadata({ url });
627
+ const title = metadataResult.title || 'Unknown';
628
+ const channel = metadataResult.channel || 'Unknown';
629
+ // Build prompt based on style
630
+ let promptStyle = '';
631
+ switch (style) {
632
+ case 'detailed':
633
+ promptStyle = 'Provide a detailed summary broken into sections covering the main topics discussed. Include key points under each section.';
634
+ break;
635
+ case 'bullets':
636
+ promptStyle = 'Provide a summary as a bulleted list of the key points and takeaways.';
637
+ break;
638
+ default: // brief
639
+ promptStyle = 'Provide a concise 2-3 paragraph summary of the main content and key takeaways.';
640
+ }
641
+ const prompt = `You are analyzing a YouTube video transcript.
642
+
643
+ Video: "${title}" by ${channel}
644
+
645
+ ${promptStyle}
646
+
647
+ Transcript:
648
+ ${transcriptResult.text.slice(0, 30000)}${transcriptResult.text.length > 30000 ? '\n\n[Transcript truncated...]' : ''}`;
649
+ // Call Gemini
650
+ const endpoint = `${GEMINI_BASE_URL}/models/gemini-2.0-flash:generateContent?key=${GOOGLE_API_KEY}`;
651
+ const response = await fetch(endpoint, {
652
+ method: 'POST',
653
+ headers: { 'Content-Type': 'application/json' },
654
+ body: JSON.stringify({
655
+ contents: [{ parts: [{ text: prompt }] }],
656
+ }),
657
+ });
658
+ if (!response.ok) {
659
+ const errorText = await response.text();
660
+ throw new Error(`Gemini API error: ${errorText}`);
661
+ }
662
+ const data = await response.json();
663
+ const summary = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
664
+ return {
665
+ success: true,
666
+ videoId,
667
+ title,
668
+ channel,
669
+ style,
670
+ summary: summary.trim(),
671
+ };
672
+ }
673
+ // ============================================================================
674
+ // Full Analysis (Hybrid)
675
+ // ============================================================================
676
+ async function analyze(params) {
677
+ const { url, mode = 'transcript', questions, extractCode = false, outputPath } = params;
678
+ if (!url || typeof url !== 'string') {
679
+ throw new Error('url is required');
680
+ }
681
+ if (!GOOGLE_API_KEY) {
682
+ throw new Error('GOOGLE_API_KEY environment variable is not set');
683
+ }
684
+ const videoId = extractVideoId(url);
685
+ ensureDirectories();
686
+ const result = {
687
+ success: true,
688
+ videoId,
689
+ mode,
690
+ url: `https://www.youtube.com/watch?v=${videoId}`,
691
+ };
692
+ // Get metadata
693
+ const metadata = await getMetadata({ url });
694
+ result.metadata = {
695
+ title: metadata.title,
696
+ channel: metadata.channel,
697
+ duration: metadata.duration,
698
+ publishedAt: metadata.publishedAt,
699
+ };
700
+ // Get chapters if available
701
+ const chapters = await getChapters({ url });
702
+ if (chapters.hasChapters) {
703
+ result.chapters = chapters.chapters;
704
+ }
705
+ // Get transcript
706
+ let transcriptText = '';
707
+ try {
708
+ const transcript = await getTranscript({ url, format: 'text' });
709
+ transcriptText = transcript.text || '';
710
+ result.transcriptLength = transcriptText.length;
711
+ }
712
+ catch (error) {
713
+ result.transcriptError = error instanceof Error ? error.message : String(error);
714
+ }
715
+ // Transcript-based analysis
716
+ if ((mode === 'transcript' || mode === 'full') && transcriptText) {
717
+ const questionsText = Array.isArray(questions) && questions.length > 0
718
+ ? `\n\nSpecific questions to answer:\n${questions.map((q, i) => `${i + 1}. ${q}`).join('\n')}`
719
+ : '';
720
+ const codeExtractionText = extractCode
721
+ ? '\n\nAlso identify and extract any code snippets, commands, or technical configurations mentioned.'
722
+ : '';
723
+ const analysisPrompt = `Analyze this YouTube video transcript comprehensively.
724
+
725
+ Video: "${metadata.title}" by ${metadata.channel}
726
+ Duration: ${metadata.duration}
727
+
728
+ Provide:
729
+ 1. A detailed summary of the content
730
+ 2. Key points and takeaways
731
+ 3. Main topics/themes discussed
732
+ 4. Any tools, technologies, or resources mentioned
733
+ 5. Target audience and skill level${questionsText}${codeExtractionText}
734
+
735
+ Transcript:
736
+ ${transcriptText.slice(0, 40000)}${transcriptText.length > 40000 ? '\n\n[Transcript truncated...]' : ''}`;
737
+ const response = await fetch(`${GEMINI_BASE_URL}/models/gemini-2.0-flash:generateContent?key=${GOOGLE_API_KEY}`, {
738
+ method: 'POST',
739
+ headers: { 'Content-Type': 'application/json' },
740
+ body: JSON.stringify({
741
+ contents: [{ parts: [{ text: analysisPrompt }] }],
742
+ }),
743
+ });
744
+ if (response.ok) {
745
+ const data = await response.json();
746
+ result.transcriptAnalysis = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
747
+ }
748
+ }
749
+ // Visual analysis with Gemini (if mode is visual or full)
750
+ if (mode === 'visual' || mode === 'full') {
751
+ // Download video for visual analysis
752
+ try {
753
+ const downloadResult = await downloadVideo({ url, quality: '480p' });
754
+ if (downloadResult.path && fs.existsSync(downloadResult.path)) {
755
+ // Upload video to Gemini Files API
756
+ const videoContent = fs.readFileSync(downloadResult.path);
757
+ const uploadResponse = await fetch(`https://generativelanguage.googleapis.com/upload/v1beta/files?key=${GOOGLE_API_KEY}`, {
758
+ method: 'POST',
759
+ headers: {
760
+ 'Content-Type': 'video/mp4',
761
+ 'X-Goog-Upload-Protocol': 'raw',
762
+ 'X-Goog-Upload-Command': 'upload, finalize',
763
+ },
764
+ body: videoContent,
765
+ });
766
+ if (uploadResponse.ok) {
767
+ const uploadData = await uploadResponse.json();
768
+ const fileUri = uploadData.file?.uri;
769
+ if (fileUri) {
770
+ // Wait for processing
771
+ await new Promise(resolve => setTimeout(resolve, 5000));
772
+ // Analyze with Gemini
773
+ const visualPrompt = `Analyze this video comprehensively. Focus on:
774
+ 1. Visual demonstrations and what's being shown on screen
775
+ 2. Any text, code, diagrams, or UI elements visible
776
+ 3. Key visual moments and their significance
777
+ 4. Production quality and presentation style
778
+
779
+ ${Array.isArray(questions) && questions.length > 0 ? `\nAnswer these specific questions:\n${questions.map((q, i) => `${i + 1}. ${q}`).join('\n')}` : ''}`;
780
+ const visualResponse = await fetch(`${GEMINI_BASE_URL}/models/gemini-2.0-flash:generateContent?key=${GOOGLE_API_KEY}`, {
781
+ method: 'POST',
782
+ headers: { 'Content-Type': 'application/json' },
783
+ body: JSON.stringify({
784
+ contents: [{
785
+ parts: [
786
+ { fileData: { mimeType: 'video/mp4', fileUri } },
787
+ { text: visualPrompt },
788
+ ],
789
+ }],
790
+ }),
791
+ });
792
+ if (visualResponse.ok) {
793
+ const visualData = await visualResponse.json();
794
+ result.visualAnalysis = visualData.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
795
+ }
796
+ }
797
+ }
798
+ // Cleanup downloaded video (optional - keep if user might want it)
799
+ // fs.unlinkSync(downloadResult.path);
800
+ result.downloadedVideoPath = downloadResult.path;
801
+ }
802
+ }
803
+ catch (error) {
804
+ result.visualAnalysisError = error instanceof Error ? error.message : String(error);
805
+ }
806
+ }
807
+ // Save to file if requested
808
+ if (outputPath && typeof outputPath === 'string') {
809
+ const dir = path.dirname(outputPath);
810
+ if (!fs.existsSync(dir)) {
811
+ fs.mkdirSync(dir, { recursive: true });
812
+ }
813
+ fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
814
+ result.savedTo = outputPath;
815
+ }
816
+ return result;
817
+ }
818
+ // ============================================================================
819
+ // Extract Code
820
+ // ============================================================================
821
+ async function extractCodeFromVideo(params) {
822
+ const { url, language } = params;
823
+ if (!url || typeof url !== 'string') {
824
+ throw new Error('url is required');
825
+ }
826
+ if (!GOOGLE_API_KEY) {
827
+ throw new Error('GOOGLE_API_KEY environment variable is not set');
828
+ }
829
+ const videoId = extractVideoId(url);
830
+ // Get transcript
831
+ const transcript = await getTranscript({ url, format: 'text' });
832
+ const transcriptText = transcript.text || '';
833
+ if (!transcriptText) {
834
+ throw new Error('No transcript available for code extraction');
835
+ }
836
+ const langHint = language && typeof language === 'string'
837
+ ? `Focus especially on ${language} code.`
838
+ : 'Detect the programming language automatically.';
839
+ const prompt = `Extract all code snippets, commands, and technical configurations from this programming video transcript.
840
+
841
+ ${langHint}
842
+
843
+ For each code snippet found:
844
+ 1. Identify what it does
845
+ 2. Extract the complete code
846
+ 3. Note any dependencies or requirements mentioned
847
+ 4. Include the approximate timestamp context if mentioned
848
+
849
+ Format each code block with proper markdown syntax highlighting.
850
+
851
+ Transcript:
852
+ ${transcriptText.slice(0, 30000)}`;
853
+ const response = await fetch(`${GEMINI_BASE_URL}/models/gemini-2.0-flash:generateContent?key=${GOOGLE_API_KEY}`, {
854
+ method: 'POST',
855
+ headers: { 'Content-Type': 'application/json' },
856
+ body: JSON.stringify({
857
+ contents: [{ parts: [{ text: prompt }] }],
858
+ }),
859
+ });
860
+ if (!response.ok) {
861
+ const errorText = await response.text();
862
+ throw new Error(`Gemini API error: ${errorText}`);
863
+ }
864
+ const data = await response.json();
865
+ const extraction = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
866
+ return {
867
+ success: true,
868
+ videoId,
869
+ language: language || 'auto-detected',
870
+ extraction: extraction.trim(),
871
+ };
872
+ }
873
+ // ============================================================================
874
+ // Backend Export
875
+ // ============================================================================
876
+ export const youtubeBackend = {
877
+ name: 'youtube',
878
+ description: 'YouTube video analysis: metadata, transcripts, chapters, downloads, and AI-powered analysis with Gemini.',
879
+ actions,
880
+ async execute(action, params) {
881
+ switch (action) {
882
+ // Metadata
883
+ case 'get_metadata':
884
+ return getMetadata(params);
885
+ // Transcript
886
+ case 'get_transcript':
887
+ return getTranscript(params);
888
+ // Chapters
889
+ case 'get_chapters':
890
+ return getChapters(params);
891
+ // Downloads
892
+ case 'download_audio':
893
+ return downloadAudio(params);
894
+ case 'download_video':
895
+ return downloadVideo(params);
896
+ // Analysis
897
+ case 'summarize':
898
+ return summarize(params);
899
+ case 'analyze':
900
+ return analyze(params);
901
+ case 'extract_code':
902
+ return extractCodeFromVideo(params);
903
+ default:
904
+ throw new Error('Unknown action: ' + action);
905
+ }
906
+ },
907
+ async healthCheck() {
908
+ const startTime = Date.now();
909
+ const issues = [];
910
+ // Check yt-dlp
911
+ const hasYtDlp = await checkYtDlp();
912
+ if (!hasYtDlp) {
913
+ issues.push('yt-dlp not installed');
914
+ }
915
+ // Check Google API key (for analysis features)
916
+ if (!GOOGLE_API_KEY) {
917
+ issues.push('GOOGLE_API_KEY not set (needed for analysis)');
918
+ }
919
+ // YouTube API key is optional
920
+ if (!YOUTUBE_API_KEY) {
921
+ // Not an error, just informational
922
+ }
923
+ if (issues.length > 0) {
924
+ return {
925
+ status: issues.includes('yt-dlp not installed') ? 'unavailable' : 'degraded',
926
+ latency_ms: Date.now() - startTime,
927
+ error: issues.join('; '),
928
+ };
929
+ }
930
+ return {
931
+ status: 'healthy',
932
+ latency_ms: Date.now() - startTime,
933
+ };
934
+ },
935
+ };
936
+ //# sourceMappingURL=youtube.js.map