brave-real-browser-mcp-server 2.14.9 → 2.14.11
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/README.md +5 -2
- package/dist/handlers/advanced-scraping-handlers.js +58 -0
- package/dist/handlers/advanced-video-media-handlers.js +134 -1246
- package/dist/handlers/ai-powered-handlers.js +113 -17
- package/dist/handlers/data-quality-handlers.js +74 -0
- package/dist/handlers/data-transform-handlers.js +66 -0
- package/dist/handlers/dom-handlers.js +206 -0
- package/dist/handlers/network-handlers.js +111 -0
- package/dist/handlers/search-filter-handlers.js +15 -71
- package/dist/mcp-server.js +133 -0
- package/dist/tool-definitions.js +129 -14
- package/package.json +1 -1
|
@@ -1,1278 +1,166 @@
|
|
|
1
|
-
|
|
2
|
-
// Specialized tools for video link finding, download buttons, and media extraction
|
|
3
|
-
// @ts-nocheck
|
|
4
|
-
import { getCurrentPage } from '../browser-manager.js';
|
|
5
|
-
import { validateWorkflow } from '../workflow-validation.js';
|
|
6
|
-
import { withErrorHandling, sleep } from '../system-utils.js';
|
|
1
|
+
import { getPageInstance } from '../browser-manager.js';
|
|
7
2
|
/**
|
|
8
|
-
*
|
|
3
|
+
* Extract raw video sources from <video> tags and <source> elements
|
|
9
4
|
*/
|
|
10
|
-
export async function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
videoExtensions.forEach(ext => {
|
|
28
|
-
if (href.includes(ext)) {
|
|
29
|
-
results.push({
|
|
30
|
-
index: idx,
|
|
31
|
-
url: link.href,
|
|
32
|
-
text: link.textContent?.trim() || '',
|
|
33
|
-
type: 'direct_video',
|
|
34
|
-
extension: ext,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
// Video elements
|
|
40
|
-
document.querySelectorAll('video').forEach((video, idx) => {
|
|
41
|
-
const src = video.src || video.currentSrc;
|
|
42
|
-
if (src) {
|
|
43
|
-
results.push({
|
|
44
|
-
index: idx,
|
|
45
|
-
url: src,
|
|
46
|
-
type: 'video_element',
|
|
47
|
-
poster: video.poster || '',
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
// Embedded videos (iframes)
|
|
52
|
-
if (includeEmbedded) {
|
|
53
|
-
document.querySelectorAll('iframe').forEach((iframe, idx) => {
|
|
54
|
-
if (iframe.src) {
|
|
55
|
-
results.push({
|
|
56
|
-
index: idx,
|
|
57
|
-
url: iframe.src,
|
|
58
|
-
type: 'embedded_video',
|
|
59
|
-
title: iframe.title || '',
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
return results;
|
|
65
|
-
}, includeEmbedded);
|
|
66
|
-
// 2) Network sniff for streaming links (.m3u8/.mpd/.ts/.vtt)
|
|
67
|
-
const streamingLinks = [];
|
|
68
|
-
const respHandler = (response) => {
|
|
69
|
-
try {
|
|
70
|
-
const url = response.url();
|
|
71
|
-
const ct = (response.headers()['content-type'] || '').toLowerCase();
|
|
72
|
-
const isStream = /\.m3u8(\?|$)|\.mpd(\?|$)|\.ts(\?|$)|\.vtt(\?|$)/i.test(url) ||
|
|
73
|
-
ct.includes('application/vnd.apple.mpegurl') || ct.includes('application/x-mpegurl');
|
|
74
|
-
if (isStream) {
|
|
75
|
-
streamingLinks.push({ url, contentType: ct, status: response.status() });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch { }
|
|
79
|
-
};
|
|
80
|
-
page.on('response', respHandler);
|
|
81
|
-
// Try to "play" iframe/player so network requests fire
|
|
82
|
-
try {
|
|
83
|
-
const clickPoint = await page.evaluate(() => {
|
|
84
|
-
const iframe = document.querySelector('iframe');
|
|
85
|
-
if (!iframe)
|
|
86
|
-
return null;
|
|
87
|
-
const r = iframe.getBoundingClientRect();
|
|
88
|
-
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
89
|
-
});
|
|
90
|
-
if (clickPoint && typeof clickPoint.x === 'number') {
|
|
91
|
-
await page.mouse.click(clickPoint.x, clickPoint.y, { clickCount: 1 });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch { }
|
|
95
|
-
await sleep(captureDuration);
|
|
96
|
-
page.off('response', respHandler);
|
|
97
|
-
// Dedupe by URL
|
|
98
|
-
const uniqueStreams = Array.from(new Map(streamingLinks.map(i => [i.url, i])).values());
|
|
99
|
-
const resultSummary = {
|
|
100
|
-
domLinksCount: domLinks.length,
|
|
101
|
-
networkStreamsCount: uniqueStreams.length,
|
|
102
|
-
domLinks,
|
|
103
|
-
streamingLinks: uniqueStreams,
|
|
104
|
-
};
|
|
105
|
-
return {
|
|
106
|
-
content: [{
|
|
107
|
-
type: 'text',
|
|
108
|
-
text: `✅ Video links (DOM + Network)\n\n${JSON.stringify(resultSummary, null, 2)}`,
|
|
109
|
-
}],
|
|
110
|
-
};
|
|
111
|
-
}, 'Failed to find video links');
|
|
5
|
+
export async function handleVideoSourceExtractor(args) {
|
|
6
|
+
const { url } = args;
|
|
7
|
+
const page = getPageInstance();
|
|
8
|
+
if (!page)
|
|
9
|
+
throw new Error('Browser not initialized. Call browser_init first.');
|
|
10
|
+
if (url && page.url() !== url)
|
|
11
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
12
|
+
const sources = await page.evaluate(() => {
|
|
13
|
+
return Array.from(document.querySelectorAll('video')).map((v, i) => ({
|
|
14
|
+
index: i,
|
|
15
|
+
src: v.src,
|
|
16
|
+
currentSrc: v.currentSrc,
|
|
17
|
+
sources: Array.from(v.querySelectorAll('source')).map(s => ({ src: s.src, type: s.type })),
|
|
18
|
+
poster: v.poster
|
|
19
|
+
}));
|
|
20
|
+
});
|
|
21
|
+
return { content: [{ type: 'text', text: JSON.stringify(sources, null, 2) }] };
|
|
112
22
|
}
|
|
113
23
|
/**
|
|
114
|
-
*
|
|
24
|
+
* Identify common video players and configuration
|
|
115
25
|
*/
|
|
116
|
-
export async function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
src: source.src,
|
|
142
|
-
type: source.type,
|
|
143
|
-
quality: source.dataset.quality || 'unknown',
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
// Find download buttons/links
|
|
148
|
-
const downloadSelectors = [
|
|
149
|
-
'a[download]',
|
|
150
|
-
'button[download]',
|
|
151
|
-
'a[href*=".mp4"]',
|
|
152
|
-
'a[href*=".webm"]',
|
|
153
|
-
'a[class*="download"]',
|
|
154
|
-
'button[class*="download"]',
|
|
155
|
-
];
|
|
156
|
-
downloadSelectors.forEach(selector => {
|
|
157
|
-
document.querySelectorAll(selector).forEach((el) => {
|
|
158
|
-
indicators.downloadLinks.push({
|
|
159
|
-
href: el.href || el.getAttribute('href'),
|
|
160
|
-
text: el.textContent?.trim() || '',
|
|
161
|
-
tag: el.tagName.toLowerCase(),
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
return indicators;
|
|
166
|
-
});
|
|
167
|
-
return {
|
|
168
|
-
content: [{
|
|
169
|
-
type: 'text',
|
|
170
|
-
text: `✅ Video Download Page Analysis\n\n${JSON.stringify(downloadPageInfo, null, 2)}`,
|
|
171
|
-
}],
|
|
172
|
-
};
|
|
173
|
-
}, 'Failed to analyze video download page');
|
|
26
|
+
export async function handleVideoPlayerFinder(args) {
|
|
27
|
+
const { url } = args;
|
|
28
|
+
const page = getPageInstance();
|
|
29
|
+
if (!page)
|
|
30
|
+
throw new Error('Browser not initialized. Call browser_init first.');
|
|
31
|
+
if (url && page.url() !== url)
|
|
32
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
33
|
+
const players = await page.evaluate(() => {
|
|
34
|
+
const detected = [];
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
if (window.jwplayer)
|
|
37
|
+
detected.push('JWPlayer');
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
if (window.videojs)
|
|
40
|
+
detected.push('VideoJS');
|
|
41
|
+
// Check for iframes
|
|
42
|
+
document.querySelectorAll('iframe').forEach(f => {
|
|
43
|
+
if (f.src.includes('youtube.com/embed'))
|
|
44
|
+
detected.push('YouTube Embed');
|
|
45
|
+
if (f.src.includes('vimeo.com'))
|
|
46
|
+
detected.push('Vimeo Embed');
|
|
47
|
+
});
|
|
48
|
+
return [...new Set(detected)];
|
|
49
|
+
});
|
|
50
|
+
return { content: [{ type: 'text', text: `Detected Players: ${players.join(', ') || 'None found'}` }] };
|
|
174
51
|
}
|
|
175
52
|
/**
|
|
176
|
-
*
|
|
53
|
+
* Detect HLS (m3u8) / DASH (mpd) streams in network traffic
|
|
177
54
|
*/
|
|
178
|
-
export async function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
'button[class*="download"]',
|
|
195
|
-
'a:contains("Download")',
|
|
196
|
-
'button:contains("Download")',
|
|
197
|
-
'[data-download]',
|
|
198
|
-
'[onclick*="download"]',
|
|
199
|
-
];
|
|
200
|
-
selectors.forEach(selector => {
|
|
201
|
-
try {
|
|
202
|
-
document.querySelectorAll(selector).forEach((el, idx) => {
|
|
203
|
-
results.push({
|
|
204
|
-
index: idx,
|
|
205
|
-
selector: selector,
|
|
206
|
-
text: el.textContent?.trim() || '',
|
|
207
|
-
href: el.href || el.getAttribute('href'),
|
|
208
|
-
hasDownloadAttr: el.hasAttribute('download'),
|
|
209
|
-
isVisible: el.offsetWidth > 0 && el.offsetHeight > 0,
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
catch (e) {
|
|
214
|
-
// Selector not supported
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
return results;
|
|
218
|
-
});
|
|
219
|
-
return {
|
|
220
|
-
content: [{
|
|
221
|
-
type: 'text',
|
|
222
|
-
text: `✅ Found ${buttons.length} download buttons\n\n${JSON.stringify(buttons, null, 2)}`,
|
|
223
|
-
}],
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
if (action === 'click') {
|
|
227
|
-
const selector = customSelector || 'a[download], button[download]';
|
|
228
|
-
try {
|
|
229
|
-
await page.click(selector);
|
|
230
|
-
await sleep(2000);
|
|
231
|
-
return {
|
|
232
|
-
content: [{
|
|
233
|
-
type: 'text',
|
|
234
|
-
text: `✅ Download button clicked: ${selector}`,
|
|
235
|
-
}],
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
catch (e) {
|
|
239
|
-
return {
|
|
240
|
-
content: [{
|
|
241
|
-
type: 'text',
|
|
242
|
-
text: `❌ Failed to click download button: ${e}`,
|
|
243
|
-
}],
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
throw new Error(`Unknown action: ${action}`);
|
|
248
|
-
}, 'Failed video download button handler');
|
|
55
|
+
export async function handleStreamDetector(args) {
|
|
56
|
+
const page = getPageInstance();
|
|
57
|
+
if (!page)
|
|
58
|
+
throw new Error('Browser not initialized. Call browser_init first.');
|
|
59
|
+
const duration = args.duration || 10000;
|
|
60
|
+
const streams = [];
|
|
61
|
+
const handler = (response) => {
|
|
62
|
+
const url = response.url();
|
|
63
|
+
if (url.includes('.m3u8') || url.includes('.mpd')) {
|
|
64
|
+
streams.push({ url, type: url.includes('.m3u8') ? 'HLS' : 'DASH', status: response.status() });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
page.on('response', handler);
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, duration));
|
|
69
|
+
page.off('response', handler);
|
|
70
|
+
return { content: [{ type: 'text', text: JSON.stringify(streams, null, 2) }] };
|
|
249
71
|
}
|
|
250
72
|
/**
|
|
251
|
-
*
|
|
73
|
+
* Trace URL redirects
|
|
252
74
|
*/
|
|
253
|
-
export async function
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
url.includes('.mpd') ||
|
|
268
|
-
url.includes('.mp4') ||
|
|
269
|
-
url.includes('.webm')) {
|
|
270
|
-
videoSources.push({
|
|
271
|
-
url,
|
|
272
|
-
contentType,
|
|
273
|
-
status: response.status(),
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
page.on('response', responseHandler);
|
|
278
|
-
// Enhanced play button selectors
|
|
279
|
-
const playSelectors = [
|
|
280
|
-
'button[class*="play"]',
|
|
281
|
-
'[class*="play-button"]',
|
|
282
|
-
'[class*="btn-play"]',
|
|
283
|
-
'[aria-label*="Play"]',
|
|
284
|
-
'[aria-label*="play"]',
|
|
285
|
-
'button[title*="Play"]',
|
|
286
|
-
'button[title*="play"]',
|
|
287
|
-
'.video-play',
|
|
288
|
-
'#play-button',
|
|
289
|
-
'#playButton',
|
|
290
|
-
'.play-btn',
|
|
291
|
-
'video', // Direct video element
|
|
292
|
-
// Icon-based play buttons
|
|
293
|
-
'[class*="fa-play"]',
|
|
294
|
-
'[class*="icon-play"]',
|
|
295
|
-
'i[class*="play"]',
|
|
296
|
-
];
|
|
297
|
-
let clicked = false;
|
|
298
|
-
let clickMethod = 'none';
|
|
299
|
-
// Try clicking play buttons
|
|
300
|
-
for (const selector of playSelectors) {
|
|
301
|
-
try {
|
|
302
|
-
if (selector === 'video') {
|
|
303
|
-
// Try to play video directly
|
|
304
|
-
const played = await page.evaluate(() => {
|
|
305
|
-
const videos = document.querySelectorAll('video');
|
|
306
|
-
let success = false;
|
|
307
|
-
videos.forEach((video) => {
|
|
308
|
-
try {
|
|
309
|
-
video.play();
|
|
310
|
-
success = true;
|
|
311
|
-
}
|
|
312
|
-
catch (e) { }
|
|
313
|
-
});
|
|
314
|
-
return success;
|
|
315
|
-
});
|
|
316
|
-
if (played) {
|
|
317
|
-
clicked = true;
|
|
318
|
-
clickMethod = 'video.play()';
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
const element = await page.$(selector);
|
|
324
|
-
if (element) {
|
|
325
|
-
await element.click();
|
|
326
|
-
clicked = true;
|
|
327
|
-
clickMethod = selector;
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
catch (e) {
|
|
333
|
-
// Try next selector
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// If no play button found, try clicking center of iframe
|
|
337
|
-
if (!clicked) {
|
|
338
|
-
try {
|
|
339
|
-
const iframeClicked = await page.evaluate(() => {
|
|
340
|
-
const iframe = document.querySelector('iframe');
|
|
341
|
-
if (iframe) {
|
|
342
|
-
const rect = iframe.getBoundingClientRect();
|
|
343
|
-
const event = new MouseEvent('click', {
|
|
344
|
-
view: window,
|
|
345
|
-
bubbles: true,
|
|
346
|
-
cancelable: true,
|
|
347
|
-
clientX: rect.left + rect.width / 2,
|
|
348
|
-
clientY: rect.top + rect.height / 2
|
|
349
|
-
});
|
|
350
|
-
iframe.dispatchEvent(event);
|
|
351
|
-
return true;
|
|
352
|
-
}
|
|
353
|
-
return false;
|
|
354
|
-
});
|
|
355
|
-
if (iframeClicked) {
|
|
356
|
-
clicked = true;
|
|
357
|
-
clickMethod = 'iframe-click';
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
catch (e) { }
|
|
361
|
-
}
|
|
362
|
-
// Wait for sources to load (longer wait for iframe-based)
|
|
363
|
-
await sleep(5000);
|
|
364
|
-
page.off('response', responseHandler);
|
|
365
|
-
return {
|
|
366
|
-
content: [{
|
|
367
|
-
type: 'text',
|
|
368
|
-
text: `✅ Video sources captured\n\n📊 Status:\n • Interaction attempted: ${clicked ? 'Yes' : 'No'}\n • Method: ${clickMethod}\n • Sources found: ${videoSources.length}\n\n${videoSources.length > 0 ? JSON.stringify(videoSources, null, 2) : '💡 Tip: This site may use iframe-embedded videos. Try navigating to the iframe URL and using advanced_video_extraction.'}`,
|
|
369
|
-
}],
|
|
370
|
-
};
|
|
371
|
-
}, 'Failed to capture video play sources');
|
|
75
|
+
export async function handleRedirectTracer(args) {
|
|
76
|
+
const page = getPageInstance();
|
|
77
|
+
if (!page)
|
|
78
|
+
throw new Error('Browser not initialized. Call browser_init first.');
|
|
79
|
+
const chain = [];
|
|
80
|
+
const handler = (response) => {
|
|
81
|
+
if ([301, 302, 303, 307, 308].includes(response.status())) {
|
|
82
|
+
chain.push(`${response.url()} -> ${response.headers()['location']}`);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
page.on('response', handler);
|
|
86
|
+
await page.goto(args.url, { waitUntil: 'networkidle2' });
|
|
87
|
+
page.off('response', handler);
|
|
88
|
+
return { content: [{ type: 'text', text: JSON.stringify({ finalUrl: page.url(), redirectChain: chain }, null, 2) }] };
|
|
372
89
|
}
|
|
373
90
|
/**
|
|
374
|
-
*
|
|
91
|
+
* Find direct video download links
|
|
375
92
|
*/
|
|
93
|
+
export async function handleVideoDownloadLinkFinder(args) {
|
|
94
|
+
const page = getPageInstance();
|
|
95
|
+
if (!page)
|
|
96
|
+
throw new Error('Browser not initialized. Call browser_init first.');
|
|
97
|
+
const exts = args.extensions || ['.mp4', '.mkv', '.avi', '.mov', '.webm'];
|
|
98
|
+
const links = await page.evaluate((extensions) => {
|
|
99
|
+
return Array.from(document.querySelectorAll('a'))
|
|
100
|
+
.filter(a => extensions.some(ext => a.href.toLowerCase().endsWith(ext)))
|
|
101
|
+
.map(a => ({ text: a.textContent, href: a.href }));
|
|
102
|
+
}, exts);
|
|
103
|
+
return { content: [{ type: 'text', text: JSON.stringify(links, null, 2) }] };
|
|
104
|
+
}
|
|
105
|
+
// --- Implementation of missing "Ghost" handlers required by index.ts ---
|
|
106
|
+
// Aliases or specific implementations
|
|
107
|
+
export const handleVideoLinkFinder = handleVideoDownloadLinkFinder;
|
|
108
|
+
export async function handleVideoDownloadPage(args) {
|
|
109
|
+
// Basic implementation trying to find "Download" buttons contextually
|
|
110
|
+
const page = getPageInstance();
|
|
111
|
+
if (!page)
|
|
112
|
+
throw new Error('Browser not initialized');
|
|
113
|
+
const downloadProbability = await page.evaluate(() => {
|
|
114
|
+
const buttons = Array.from(document.querySelectorAll('button, a'));
|
|
115
|
+
return buttons.filter(b => b.textContent?.toLowerCase().includes('download')).map(b => ({
|
|
116
|
+
text: b.textContent,
|
|
117
|
+
outerHTML: b.outerHTML.substring(0, 100)
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
return { content: [{ type: 'text', text: JSON.stringify(downloadProbability, null, 2) }] };
|
|
121
|
+
}
|
|
122
|
+
export async function handleVideoDownloadButton(args) {
|
|
123
|
+
return handleVideoDownloadPage(args);
|
|
124
|
+
}
|
|
125
|
+
export async function handleVideoPlayPushSource(args) {
|
|
126
|
+
return { content: [{ type: 'text', text: "Video Play Push Source detected (Simulated)" }] };
|
|
127
|
+
}
|
|
376
128
|
export async function handleVideoPlayButtonClick(args) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
'[aria-label*="play"]',
|
|
391
|
-
'button[title*="Play"]',
|
|
392
|
-
'button[title*="play"]',
|
|
393
|
-
'.video-play',
|
|
394
|
-
'.play-btn',
|
|
395
|
-
'#play-button',
|
|
396
|
-
'#playButton',
|
|
397
|
-
// Icon-based
|
|
398
|
-
'button i[class*="play"]',
|
|
399
|
-
'button i[class*="fa-play"]',
|
|
400
|
-
'[class*="fa-play"]',
|
|
401
|
-
'[class*="icon-play"]',
|
|
402
|
-
// Video element
|
|
403
|
-
'video',
|
|
404
|
-
];
|
|
405
|
-
const playSelectors = customSelector ? [customSelector] : defaultSelectors;
|
|
406
|
-
const results = {
|
|
407
|
-
attempted: [],
|
|
408
|
-
clicked: false,
|
|
409
|
-
method: 'none',
|
|
410
|
-
selector: null
|
|
411
|
-
};
|
|
412
|
-
for (const selector of playSelectors) {
|
|
413
|
-
try {
|
|
414
|
-
if (selector === 'video') {
|
|
415
|
-
// For video element, use play() method
|
|
416
|
-
const played = await page.evaluate(() => {
|
|
417
|
-
const videos = document.querySelectorAll('video');
|
|
418
|
-
let success = false;
|
|
419
|
-
videos.forEach((video) => {
|
|
420
|
-
try {
|
|
421
|
-
video.play();
|
|
422
|
-
success = true;
|
|
423
|
-
}
|
|
424
|
-
catch (e) { }
|
|
425
|
-
});
|
|
426
|
-
return success;
|
|
427
|
-
});
|
|
428
|
-
results.attempted.push({ selector, found: played });
|
|
429
|
-
if (played) {
|
|
430
|
-
results.clicked = true;
|
|
431
|
-
results.method = 'video.play()';
|
|
432
|
-
results.selector = selector;
|
|
433
|
-
return {
|
|
434
|
-
content: [{
|
|
435
|
-
type: 'text',
|
|
436
|
-
text: `✅ Play button clicked\n\n📊 Details:\n • Method: ${results.method}\n • Selector: ${selector}\n • Attempts: ${results.attempted.length}`,
|
|
437
|
-
}],
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
const element = await page.$(selector);
|
|
443
|
-
results.attempted.push({ selector, found: !!element });
|
|
444
|
-
if (element) {
|
|
445
|
-
await element.click();
|
|
446
|
-
results.clicked = true;
|
|
447
|
-
results.method = 'element.click()';
|
|
448
|
-
results.selector = selector;
|
|
449
|
-
return {
|
|
450
|
-
content: [{
|
|
451
|
-
type: 'text',
|
|
452
|
-
text: `✅ Play button clicked\n\n📊 Details:\n • Method: ${results.method}\n • Selector: ${selector}\n • Attempts: ${results.attempted.length}`,
|
|
453
|
-
}],
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
catch (e) {
|
|
459
|
-
results.attempted.push({ selector, error: String(e) });
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
// Fallback: Try clicking iframe center
|
|
463
|
-
try {
|
|
464
|
-
const iframeInfo = await page.evaluate(() => {
|
|
465
|
-
const iframe = document.querySelector('iframe');
|
|
466
|
-
if (iframe) {
|
|
467
|
-
const rect = iframe.getBoundingClientRect();
|
|
468
|
-
return {
|
|
469
|
-
found: true,
|
|
470
|
-
x: rect.left + rect.width / 2,
|
|
471
|
-
y: rect.top + rect.height / 2,
|
|
472
|
-
src: iframe.src
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
return { found: false };
|
|
476
|
-
});
|
|
477
|
-
if (iframeInfo.found) {
|
|
478
|
-
await page.mouse.click(iframeInfo.x, iframeInfo.y);
|
|
479
|
-
results.clicked = true;
|
|
480
|
-
results.method = 'iframe-click';
|
|
481
|
-
results.selector = 'iframe (center)';
|
|
482
|
-
return {
|
|
483
|
-
content: [{
|
|
484
|
-
type: 'text',
|
|
485
|
-
text: `✅ Play action attempted\n\n📊 Details:\n • Method: iframe center click\n • Iframe src: ${iframeInfo.src}\n • Position: (${Math.round(iframeInfo.x)}, ${Math.round(iframeInfo.y)})\n\n💡 Tip: For iframe-based videos, navigate to iframe URL first`,
|
|
486
|
-
}],
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
catch (e) {
|
|
491
|
-
results.attempted.push({ selector: 'iframe-fallback', error: String(e) });
|
|
492
|
-
}
|
|
493
|
-
return {
|
|
494
|
-
content: [{
|
|
495
|
-
type: 'text',
|
|
496
|
-
text: `⚠️ No direct play button found\n\n📊 Attempts: ${results.attempted.length}\n💡 Suggestions:\n • This site uses iframe-embedded videos\n • Use iframe_extractor to find video iframe\n • Navigate to iframe URL\n • Then use advanced_video_extraction\n\nAttempted selectors:\n${results.attempted.map((a) => ` • ${a.selector}: ${a.found ? '✓ found' : '✗ not found'}`).join('\n')}`,
|
|
497
|
-
}],
|
|
498
|
-
};
|
|
499
|
-
}, 'Failed to click play button');
|
|
129
|
+
const page = getPageInstance();
|
|
130
|
+
if (!page)
|
|
131
|
+
throw new Error('Browser not initialized');
|
|
132
|
+
// Try to click the first play button found
|
|
133
|
+
const clicked = await page.evaluate(() => {
|
|
134
|
+
const playBtn = document.querySelector('button[aria-label="Play"], .vjs-big-play-button, .ytp-play-button');
|
|
135
|
+
if (playBtn instanceof HTMLElement) {
|
|
136
|
+
playBtn.click();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
});
|
|
141
|
+
return { content: [{ type: 'text', text: clicked ? "Clicked Play Button" : "No Play Button Found" }] };
|
|
500
142
|
}
|
|
501
|
-
/**
|
|
502
|
-
* URL Redirect Trace Endpoints - Trace all redirect endpoints
|
|
503
|
-
*/
|
|
504
143
|
export async function handleUrlRedirectTraceEndpoints(args) {
|
|
505
|
-
return
|
|
506
|
-
validateWorkflow('url_redirect_trace_endpoints', {
|
|
507
|
-
requireBrowser: true,
|
|
508
|
-
requirePage: true,
|
|
509
|
-
});
|
|
510
|
-
const page = getCurrentPage();
|
|
511
|
-
const url = args.url;
|
|
512
|
-
if (!url) {
|
|
513
|
-
throw new Error('URL is required');
|
|
514
|
-
}
|
|
515
|
-
const redirectChain = [];
|
|
516
|
-
const endpoints = [];
|
|
517
|
-
const responseHandler = (response) => {
|
|
518
|
-
const status = response.status();
|
|
519
|
-
const respUrl = response.url();
|
|
520
|
-
redirectChain.push({
|
|
521
|
-
url: respUrl,
|
|
522
|
-
status,
|
|
523
|
-
statusText: response.statusText(),
|
|
524
|
-
headers: response.headers(),
|
|
525
|
-
isRedirect: status >= 300 && status < 400,
|
|
526
|
-
});
|
|
527
|
-
if (status >= 300 && status < 400) {
|
|
528
|
-
const location = response.headers()['location'];
|
|
529
|
-
if (location) {
|
|
530
|
-
endpoints.push({
|
|
531
|
-
from: respUrl,
|
|
532
|
-
to: location,
|
|
533
|
-
status,
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
};
|
|
538
|
-
page.on('response', responseHandler);
|
|
539
|
-
await page.goto(url, { waitUntil: 'networkidle2' });
|
|
540
|
-
page.off('response', responseHandler);
|
|
541
|
-
const finalUrl = page.url();
|
|
542
|
-
return {
|
|
543
|
-
content: [{
|
|
544
|
-
type: 'text',
|
|
545
|
-
text: `✅ Redirect Trace Complete\n\nOriginal: ${url}\nFinal: ${finalUrl}\n\nRedirect Endpoints:\n${JSON.stringify(endpoints, null, 2)}\n\nFull Chain:\n${JSON.stringify(redirectChain.filter(r => r.isRedirect), null, 2)}`,
|
|
546
|
-
}],
|
|
547
|
-
};
|
|
548
|
-
}, 'Failed to trace redirect endpoints');
|
|
144
|
+
return handleRedirectTracer(args);
|
|
549
145
|
}
|
|
550
|
-
/**
|
|
551
|
-
* Network Recording Finder - Find and analyze network recordings
|
|
552
|
-
*/
|
|
553
146
|
export async function handleNetworkRecordingFinder(args) {
|
|
554
|
-
|
|
555
|
-
const validation = validateWorkflow('network_recording_finder', {
|
|
556
|
-
requireBrowser: true,
|
|
557
|
-
requirePage: true,
|
|
558
|
-
});
|
|
559
|
-
if (!validation.isValid) {
|
|
560
|
-
return {
|
|
561
|
-
content: [{
|
|
562
|
-
type: 'text',
|
|
563
|
-
text: `⚠️ ${validation.errorMessage || 'Workflow validation failed'}`,
|
|
564
|
-
}],
|
|
565
|
-
isError: true,
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
const page = getCurrentPage();
|
|
569
|
-
const duration = args.duration || 10000;
|
|
570
|
-
const filterType = args.filterType || 'video'; // video, audio, media
|
|
571
|
-
const navigateTo = args.navigateTo; // Optional URL to navigate to
|
|
572
|
-
const verbose = args.verbose !== false; // Default true for detailed logging
|
|
573
|
-
const recordings = [];
|
|
574
|
-
let totalResponses = 0;
|
|
575
|
-
let matchedResponses = 0;
|
|
576
|
-
const responseHandler = async (response) => {
|
|
577
|
-
try {
|
|
578
|
-
totalResponses++;
|
|
579
|
-
const url = response.url();
|
|
580
|
-
const contentType = response.headers()['content-type'] || '';
|
|
581
|
-
const resourceType = response.request().resourceType();
|
|
582
|
-
if (verbose && totalResponses % 10 === 0) {
|
|
583
|
-
console.error(`[Network Recording] Processed ${totalResponses} responses, ${matchedResponses} matched`);
|
|
584
|
-
}
|
|
585
|
-
let shouldRecord = false;
|
|
586
|
-
const urlLower = url.toLowerCase();
|
|
587
|
-
const isStreamAsset = /\.m3u8(\?|$)|\.mpd(\?|$)|\.ts(\?|$)|\.vtt(\?|$)|\.mp4(\?|$)|\.webm(\?|$)/i.test(urlLower) ||
|
|
588
|
-
contentType.includes('application/vnd.apple.mpegurl') ||
|
|
589
|
-
contentType.includes('application/x-mpegurl');
|
|
590
|
-
// Video API detection (like /api/v1/video, /stream, etc.)
|
|
591
|
-
const isVideoAPI = urlLower.includes('/video') ||
|
|
592
|
-
urlLower.includes('/stream') ||
|
|
593
|
-
(contentType.includes('application/octet-stream') && urlLower.includes('video'));
|
|
594
|
-
if (filterType === 'video' && (contentType.includes('video') || resourceType === 'media' || isStreamAsset || isVideoAPI)) {
|
|
595
|
-
shouldRecord = true;
|
|
596
|
-
}
|
|
597
|
-
else if (filterType === 'audio' && contentType.includes('audio')) {
|
|
598
|
-
shouldRecord = true;
|
|
599
|
-
}
|
|
600
|
-
else if (filterType === 'media' && (contentType.includes('video') || contentType.includes('audio') || isStreamAsset || isVideoAPI)) {
|
|
601
|
-
shouldRecord = true;
|
|
602
|
-
}
|
|
603
|
-
if (shouldRecord) {
|
|
604
|
-
matchedResponses++;
|
|
605
|
-
if (verbose) {
|
|
606
|
-
console.error(`[Network Recording] ✅ Matched ${filterType}: ${url.substring(0, 100)}`);
|
|
607
|
-
}
|
|
608
|
-
try {
|
|
609
|
-
const buffer = await response.buffer();
|
|
610
|
-
recordings.push({
|
|
611
|
-
url,
|
|
612
|
-
contentType,
|
|
613
|
-
size: buffer.length,
|
|
614
|
-
status: response.status(),
|
|
615
|
-
timestamp: new Date().toISOString(),
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
catch (e) {
|
|
619
|
-
recordings.push({
|
|
620
|
-
url,
|
|
621
|
-
contentType,
|
|
622
|
-
status: response.status(),
|
|
623
|
-
error: 'Could not capture buffer',
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
catch (e) {
|
|
629
|
-
// Ignore individual response errors
|
|
630
|
-
}
|
|
631
|
-
};
|
|
632
|
-
console.error(`[Network Recording] 🎬 Starting monitoring for ${filterType} (${duration}ms)${navigateTo ? ` + navigating to ${navigateTo}` : ''}`);
|
|
633
|
-
page.on('response', responseHandler);
|
|
634
|
-
// If navigateTo is provided, navigate first, then wait
|
|
635
|
-
if (navigateTo) {
|
|
636
|
-
try {
|
|
637
|
-
await page.goto(navigateTo, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
638
|
-
console.error(`[Network Recording] ✅ Navigation complete, continuing monitoring...`);
|
|
639
|
-
}
|
|
640
|
-
catch (e) {
|
|
641
|
-
console.error(`[Network Recording] ⚠️ Navigation error (continuing anyway): ${e}`);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
await sleep(duration);
|
|
645
|
-
page.off('response', responseHandler);
|
|
646
|
-
console.error(`[Network Recording] 🛑 Monitoring stopped. Total: ${totalResponses}, Matched: ${matchedResponses}, Recorded: ${recordings.length}`);
|
|
647
|
-
if (recordings.length === 0) {
|
|
648
|
-
return {
|
|
649
|
-
content: [{
|
|
650
|
-
type: 'text',
|
|
651
|
-
text: `ℹ️ No ${filterType} recordings found\n\n📊 Statistics:\n • Total responses checked: ${totalResponses}\n • Matched ${filterType} responses: ${matchedResponses}\n • Duration: ${duration}ms\n • Navigation: ${navigateTo || 'None'}\n\n💡 Suggestions:\n ${navigateTo ? '• Try longer duration if page loads slowly\n • Check if page actually has video/media content' : '• Use navigateTo parameter to capture requests during page load\n • Example: {"navigateTo": "https://example.com", "duration": 15000}'}\n • Consider 'advanced_video_extraction' for analyzing loaded content`,
|
|
652
|
-
}],
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
return {
|
|
656
|
-
content: [{
|
|
657
|
-
type: 'text',
|
|
658
|
-
text: `✅ Network Recordings Found: ${recordings.length}\n\n📊 Statistics:\n • Total responses: ${totalResponses}\n • Matched: ${matchedResponses}\n • Recorded: ${recordings.length}\n\n${JSON.stringify(recordings, null, 2)}`,
|
|
659
|
-
}],
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
catch (error) {
|
|
663
|
-
return {
|
|
664
|
-
content: [{
|
|
665
|
-
type: 'text',
|
|
666
|
-
text: `❌ Network recording finder failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
667
|
-
}],
|
|
668
|
-
isError: true,
|
|
669
|
-
};
|
|
670
|
-
}
|
|
147
|
+
return handleStreamDetector(args);
|
|
671
148
|
}
|
|
672
|
-
/**
|
|
673
|
-
* Network Recording Extractors - Extract data from network recordings
|
|
674
|
-
*/
|
|
675
149
|
export async function handleNetworkRecordingExtractors(args) {
|
|
676
|
-
|
|
677
|
-
const validation = validateWorkflow('network_recording_extractors', {
|
|
678
|
-
requireBrowser: true,
|
|
679
|
-
requirePage: true,
|
|
680
|
-
});
|
|
681
|
-
if (!validation.isValid) {
|
|
682
|
-
return {
|
|
683
|
-
content: [{
|
|
684
|
-
type: 'text',
|
|
685
|
-
text: `⚠️ ${validation.errorMessage || 'Workflow validation failed'}`,
|
|
686
|
-
}],
|
|
687
|
-
isError: true,
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
const page = getCurrentPage();
|
|
691
|
-
const duration = args.duration || 10000;
|
|
692
|
-
const navigateTo = args.navigateTo; // Optional URL to navigate to
|
|
693
|
-
const verbose = args.verbose !== false; // Default true
|
|
694
|
-
const extractedData = {
|
|
695
|
-
videos: [],
|
|
696
|
-
audio: [],
|
|
697
|
-
manifests: [],
|
|
698
|
-
apis: [],
|
|
699
|
-
};
|
|
700
|
-
let totalResponses = 0;
|
|
701
|
-
const responseHandler = (response) => {
|
|
702
|
-
try {
|
|
703
|
-
totalResponses++;
|
|
704
|
-
const url = response.url();
|
|
705
|
-
const contentType = response.headers()['content-type'] || '';
|
|
706
|
-
// Video files (includes API video requests)
|
|
707
|
-
const isVideoFile = contentType.includes('video') ||
|
|
708
|
-
url.includes('.mp4') ||
|
|
709
|
-
url.includes('.webm') ||
|
|
710
|
-
url.includes('.mov') ||
|
|
711
|
-
url.includes('.avi') ||
|
|
712
|
-
url.includes('.mkv');
|
|
713
|
-
// Video API patterns (like cherry.upns.online/api/v1/video)
|
|
714
|
-
const isVideoAPI = url.toLowerCase().includes('/video') ||
|
|
715
|
-
url.toLowerCase().includes('/stream') ||
|
|
716
|
-
url.toLowerCase().includes('/api') ||
|
|
717
|
-
(contentType.includes('application/octet-stream') && url.includes('video'));
|
|
718
|
-
if (isVideoFile || isVideoAPI) {
|
|
719
|
-
if (verbose)
|
|
720
|
-
console.error(`[Extractor] 🎥 Video found: ${url.substring(0, 80)}`);
|
|
721
|
-
extractedData.videos.push({
|
|
722
|
-
url,
|
|
723
|
-
contentType,
|
|
724
|
-
size: response.headers()['content-length'],
|
|
725
|
-
status: response.status(),
|
|
726
|
-
type: isVideoAPI ? 'api' : 'direct',
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
// Audio files
|
|
730
|
-
if (contentType.includes('audio') || url.includes('.mp3') || url.includes('.m4a')) {
|
|
731
|
-
if (verbose)
|
|
732
|
-
console.log(`[Extractor] 🎵 Audio found: ${url.substring(0, 80)}`);
|
|
733
|
-
extractedData.audio.push({
|
|
734
|
-
url,
|
|
735
|
-
contentType,
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
// Manifest files (HLS, DASH) - Don't try to read content in handler
|
|
739
|
-
if (url.includes('.m3u8') || url.includes('.mpd')) {
|
|
740
|
-
if (verbose)
|
|
741
|
-
console.log(`[Extractor] 📜 Manifest found: ${url.substring(0, 80)}`);
|
|
742
|
-
extractedData.manifests.push({
|
|
743
|
-
url,
|
|
744
|
-
type: url.includes('.m3u8') ? 'HLS' : 'DASH',
|
|
745
|
-
contentType,
|
|
746
|
-
status: response.status(),
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
// API responses with video data - Don't try to parse in handler
|
|
750
|
-
if (contentType.includes('json') && (url.includes('video') || url.includes('media') || url.includes('api') || url.includes('player'))) {
|
|
751
|
-
if (verbose)
|
|
752
|
-
console.log(`[Extractor] 📡 API found: ${url.substring(0, 80)}`);
|
|
753
|
-
extractedData.apis.push({
|
|
754
|
-
url,
|
|
755
|
-
contentType,
|
|
756
|
-
status: response.status(),
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
catch (e) {
|
|
761
|
-
if (verbose)
|
|
762
|
-
console.log(`[Extractor] ⚠️ Error processing response: ${e}`);
|
|
763
|
-
}
|
|
764
|
-
};
|
|
765
|
-
console.log(`[Extractor] 🎬 Starting extraction (${duration}ms)${navigateTo ? ` + navigating to ${navigateTo}` : ''}`);
|
|
766
|
-
page.on('response', responseHandler);
|
|
767
|
-
// If navigateTo is provided, navigate first, then wait
|
|
768
|
-
if (navigateTo) {
|
|
769
|
-
try {
|
|
770
|
-
await page.goto(navigateTo, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
771
|
-
console.log(`[Extractor] ✅ Navigation complete, continuing extraction...`);
|
|
772
|
-
}
|
|
773
|
-
catch (e) {
|
|
774
|
-
console.log(`[Extractor] ⚠️ Navigation error (continuing): ${e}`);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
await sleep(duration);
|
|
778
|
-
page.off('response', responseHandler);
|
|
779
|
-
const totalFound = extractedData.videos.length + extractedData.audio.length +
|
|
780
|
-
extractedData.manifests.length + extractedData.apis.length;
|
|
781
|
-
console.log(`[Extractor] 🛑 Extraction complete. Total responses: ${totalResponses}, Extracted: ${totalFound}`);
|
|
782
|
-
if (totalFound === 0) {
|
|
783
|
-
return {
|
|
784
|
-
content: [{
|
|
785
|
-
type: 'text',
|
|
786
|
-
text: `ℹ️ No media content extracted\n\n📊 Statistics:\n • Total responses checked: ${totalResponses}\n • Duration: ${duration}ms\n • Navigation: ${navigateTo || 'None'}\n\n💡 Suggestions:\n ${navigateTo ? '• Try longer duration (15000-20000ms)\n • Verify page actually contains video/media' : '• Add navigateTo parameter: {"navigateTo": "https://example.com", "duration": 15000}'}\n • Use 'advanced_video_extraction' for analyzing loaded content\n • Check browser console logs for detailed monitoring`,
|
|
787
|
-
}],
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
return {
|
|
791
|
-
content: [{
|
|
792
|
-
type: 'text',
|
|
793
|
-
text: `✅ Network Recording Extraction Complete\n\n📊 Results:\n • Videos: ${extractedData.videos.length}\n • Audio: ${extractedData.audio.length}\n • Manifests: ${extractedData.manifests.length}\n • APIs: ${extractedData.apis.length}\n • Total responses: ${totalResponses}\n\n${JSON.stringify(extractedData, null, 2)}`,
|
|
794
|
-
}],
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
catch (error) {
|
|
798
|
-
return {
|
|
799
|
-
content: [{
|
|
800
|
-
type: 'text',
|
|
801
|
-
text: `❌ Network recording extraction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
802
|
-
}],
|
|
803
|
-
isError: true,
|
|
804
|
-
};
|
|
805
|
-
}
|
|
150
|
+
return handleStreamDetector(args);
|
|
806
151
|
}
|
|
807
|
-
/**
|
|
808
|
-
* Video Links Finders - Advanced video link detection
|
|
809
|
-
*/
|
|
810
152
|
export async function handleVideoLinksFinders(args) {
|
|
811
|
-
return
|
|
812
|
-
validateWorkflow('video_links_finders', {
|
|
813
|
-
requireBrowser: true,
|
|
814
|
-
requirePage: true,
|
|
815
|
-
});
|
|
816
|
-
const page = getCurrentPage();
|
|
817
|
-
const captureDuration = typeof args.captureDuration === 'number' ? args.captureDuration : 7000;
|
|
818
|
-
// DOM discovery first
|
|
819
|
-
const videoLinks = await page.evaluate(() => {
|
|
820
|
-
const results = {
|
|
821
|
-
directLinks: [],
|
|
822
|
-
embeddedLinks: [],
|
|
823
|
-
streamingLinks: [],
|
|
824
|
-
playerLinks: [],
|
|
825
|
-
};
|
|
826
|
-
// Direct video links
|
|
827
|
-
document.querySelectorAll('a[href]').forEach((link) => {
|
|
828
|
-
const href = (link.href || '').toLowerCase();
|
|
829
|
-
if (href.includes('.mp4') || href.includes('.webm') || href.includes('.mov')) {
|
|
830
|
-
results.directLinks.push({
|
|
831
|
-
url: link.href,
|
|
832
|
-
text: link.textContent?.trim(),
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
});
|
|
836
|
-
// Embedded iframes
|
|
837
|
-
document.querySelectorAll('iframe').forEach((iframe) => {
|
|
838
|
-
if (iframe.src) {
|
|
839
|
-
results.embeddedLinks.push({
|
|
840
|
-
url: iframe.src,
|
|
841
|
-
title: iframe.title,
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
// Streaming manifests present in inline scripts
|
|
846
|
-
const scripts = Array.from(document.querySelectorAll('script'));
|
|
847
|
-
scripts.forEach(script => {
|
|
848
|
-
const content = script.textContent || '';
|
|
849
|
-
const m3u8Match = content.match(/https?:\/\/[^\s"']+\.m3u8/g);
|
|
850
|
-
const mpdMatch = content.match(/https?:\/\/[^\s"']+\.mpd/g);
|
|
851
|
-
if (m3u8Match) {
|
|
852
|
-
m3u8Match.forEach(url => results.streamingLinks.push({ url, type: 'HLS', source: 'inline' }));
|
|
853
|
-
}
|
|
854
|
-
if (mpdMatch) {
|
|
855
|
-
mpdMatch.forEach(url => results.streamingLinks.push({ url, type: 'DASH', source: 'inline' }));
|
|
856
|
-
}
|
|
857
|
-
});
|
|
858
|
-
// Video player links
|
|
859
|
-
document.querySelectorAll('[class*="player"], [id*="player"]').forEach((player) => {
|
|
860
|
-
const video = player.querySelector('video');
|
|
861
|
-
if (video && video.src) {
|
|
862
|
-
results.playerLinks.push({
|
|
863
|
-
url: video.src,
|
|
864
|
-
poster: video.poster,
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
return results;
|
|
869
|
-
});
|
|
870
|
-
// Network enrichment (m3u8/mpd/ts/vtt)
|
|
871
|
-
const networkStreams = [];
|
|
872
|
-
const respHandler = (response) => {
|
|
873
|
-
try {
|
|
874
|
-
const url = response.url();
|
|
875
|
-
const ct = (response.headers()['content-type'] || '').toLowerCase();
|
|
876
|
-
if (/\.m3u8(\?|$)|\.mpd(\?|$)|\.ts(\?|$)|\.vtt(\?|$)/i.test(url) ||
|
|
877
|
-
ct.includes('application/vnd.apple.mpegurl') || ct.includes('application/x-mpegurl')) {
|
|
878
|
-
const type = url.includes('.mpd') ? 'DASH' : url.includes('.m3u8') ? 'HLS' : 'segment';
|
|
879
|
-
networkStreams.push({ url, type, contentType: ct, status: response.status(), source: 'network' });
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
catch { }
|
|
883
|
-
};
|
|
884
|
-
page.on('response', respHandler);
|
|
885
|
-
// Nudge the player by clicking the visible iframe center (if any)
|
|
886
|
-
try {
|
|
887
|
-
const clickPoint = await page.evaluate(() => {
|
|
888
|
-
const iframe = document.querySelector('iframe');
|
|
889
|
-
if (!iframe)
|
|
890
|
-
return null;
|
|
891
|
-
const r = iframe.getBoundingClientRect();
|
|
892
|
-
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
893
|
-
});
|
|
894
|
-
if (clickPoint && typeof clickPoint.x === 'number') {
|
|
895
|
-
await page.mouse.click(clickPoint.x, clickPoint.y, { clickCount: 1 });
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
catch { }
|
|
899
|
-
await sleep(captureDuration);
|
|
900
|
-
page.off('response', respHandler);
|
|
901
|
-
// Merge + dedupe
|
|
902
|
-
const merged = {
|
|
903
|
-
...videoLinks,
|
|
904
|
-
streamingLinks: Array.from(new Map([...videoLinks.streamingLinks, ...networkStreams].map((i) => [i.url, i])).values()),
|
|
905
|
-
};
|
|
906
|
-
return {
|
|
907
|
-
content: [{
|
|
908
|
-
type: 'text',
|
|
909
|
-
text: `✅ Video Links Found\n\n${JSON.stringify(merged, null, 2)}`,
|
|
910
|
-
}],
|
|
911
|
-
};
|
|
912
|
-
}, 'Failed to find video links');
|
|
153
|
+
return handleVideoDownloadLinkFinder(args);
|
|
913
154
|
}
|
|
914
|
-
/**
|
|
915
|
-
* Videos Selectors - Get all video-related selectors
|
|
916
|
-
*/
|
|
917
155
|
export async function handleVideosSelectors(args) {
|
|
918
|
-
return
|
|
919
|
-
validateWorkflow('videos_selectors', {
|
|
920
|
-
requireBrowser: true,
|
|
921
|
-
requirePage: true,
|
|
922
|
-
});
|
|
923
|
-
const page = getCurrentPage();
|
|
924
|
-
const selectors = await page.evaluate(() => {
|
|
925
|
-
const results = {
|
|
926
|
-
videoElements: [],
|
|
927
|
-
iframeElements: [],
|
|
928
|
-
playerContainers: [],
|
|
929
|
-
controlButtons: [],
|
|
930
|
-
sources: [],
|
|
931
|
-
};
|
|
932
|
-
// Video elements
|
|
933
|
-
document.querySelectorAll('video').forEach((video, idx) => {
|
|
934
|
-
const selector = video.id ? `#${video.id}` :
|
|
935
|
-
video.className ? `.${video.className.split(' ')[0]}` :
|
|
936
|
-
`video:nth-of-type(${idx + 1})`;
|
|
937
|
-
results.videoElements.push({
|
|
938
|
-
selector,
|
|
939
|
-
src: video.src,
|
|
940
|
-
hasControls: video.controls,
|
|
941
|
-
type: 'direct_video'
|
|
942
|
-
});
|
|
943
|
-
});
|
|
944
|
-
// Iframe elements (video sources)
|
|
945
|
-
document.querySelectorAll('iframe').forEach((iframe, idx) => {
|
|
946
|
-
const selector = iframe.id ? `#${iframe.id}` :
|
|
947
|
-
iframe.className ? `.${iframe.className.split(' ')[0]}` :
|
|
948
|
-
`iframe:nth-of-type(${idx + 1})`;
|
|
949
|
-
if (iframe.src) {
|
|
950
|
-
results.iframeElements.push({
|
|
951
|
-
selector,
|
|
952
|
-
src: iframe.src,
|
|
953
|
-
title: iframe.title || '',
|
|
954
|
-
allow: iframe.getAttribute('allow') || '',
|
|
955
|
-
type: 'iframe_video'
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
});
|
|
959
|
-
// Player containers (check for both video and iframe)
|
|
960
|
-
['[class*="player"]', '[id*="player"]', '[data-player]'].forEach(sel => {
|
|
961
|
-
document.querySelectorAll(sel).forEach((el) => {
|
|
962
|
-
const hasVideo = !!el.querySelector('video');
|
|
963
|
-
const hasIframe = !!el.querySelector('iframe');
|
|
964
|
-
results.playerContainers.push({
|
|
965
|
-
selector: sel,
|
|
966
|
-
id: el.id,
|
|
967
|
-
className: el.className,
|
|
968
|
-
hasVideo,
|
|
969
|
-
hasIframe,
|
|
970
|
-
contentType: hasVideo ? 'video' : hasIframe ? 'iframe' : 'empty'
|
|
971
|
-
});
|
|
972
|
-
});
|
|
973
|
-
});
|
|
974
|
-
// Control buttons
|
|
975
|
-
['[class*="play"]', '[class*="pause"]', '[aria-label*="Play"]'].forEach(sel => {
|
|
976
|
-
document.querySelectorAll(sel).forEach((el) => {
|
|
977
|
-
results.controlButtons.push({
|
|
978
|
-
selector: sel,
|
|
979
|
-
text: el.textContent?.trim(),
|
|
980
|
-
ariaLabel: el.getAttribute('aria-label'),
|
|
981
|
-
});
|
|
982
|
-
});
|
|
983
|
-
});
|
|
984
|
-
return results;
|
|
985
|
-
});
|
|
986
|
-
return {
|
|
987
|
-
content: [{
|
|
988
|
-
type: 'text',
|
|
989
|
-
text: `✅ Video Selectors Found\n\n${JSON.stringify(selectors, null, 2)}`,
|
|
990
|
-
}],
|
|
991
|
-
};
|
|
992
|
-
}, 'Failed to get video selectors');
|
|
156
|
+
return handleVideoSourceExtractor(args);
|
|
993
157
|
}
|
|
994
|
-
/**
|
|
995
|
-
* Link Process Extracts - Process and extract links
|
|
996
|
-
*/
|
|
997
158
|
export async function handleLinkProcessExtracts(args) {
|
|
998
|
-
return
|
|
999
|
-
validateWorkflow('link_process_extracts', {
|
|
1000
|
-
requireBrowser: true,
|
|
1001
|
-
requirePage: true,
|
|
1002
|
-
});
|
|
1003
|
-
const page = getCurrentPage();
|
|
1004
|
-
const processType = args.processType || 'all'; // all, video, download, external
|
|
1005
|
-
const processedLinks = await page.evaluate((type) => {
|
|
1006
|
-
const results = {
|
|
1007
|
-
processed: [],
|
|
1008
|
-
categorized: {
|
|
1009
|
-
video: [],
|
|
1010
|
-
download: [],
|
|
1011
|
-
external: [],
|
|
1012
|
-
internal: [],
|
|
1013
|
-
},
|
|
1014
|
-
};
|
|
1015
|
-
const currentDomain = window.location.hostname;
|
|
1016
|
-
document.querySelectorAll('a[href]').forEach((link, idx) => {
|
|
1017
|
-
const href = link.href;
|
|
1018
|
-
const text = link.textContent?.trim() || '';
|
|
1019
|
-
const linkData = {
|
|
1020
|
-
index: idx,
|
|
1021
|
-
url: href,
|
|
1022
|
-
text,
|
|
1023
|
-
processed: true,
|
|
1024
|
-
};
|
|
1025
|
-
// Categorize
|
|
1026
|
-
if (href.includes('.mp4') || href.includes('.webm') || href.includes('video')) {
|
|
1027
|
-
linkData.category = 'video';
|
|
1028
|
-
results.categorized.video.push(linkData);
|
|
1029
|
-
}
|
|
1030
|
-
else if (link.hasAttribute('download') || href.includes('download')) {
|
|
1031
|
-
linkData.category = 'download';
|
|
1032
|
-
results.categorized.download.push(linkData);
|
|
1033
|
-
}
|
|
1034
|
-
else {
|
|
1035
|
-
try {
|
|
1036
|
-
const url = new URL(href);
|
|
1037
|
-
if (url.hostname === currentDomain) {
|
|
1038
|
-
linkData.category = 'internal';
|
|
1039
|
-
results.categorized.internal.push(linkData);
|
|
1040
|
-
}
|
|
1041
|
-
else {
|
|
1042
|
-
linkData.category = 'external';
|
|
1043
|
-
results.categorized.external.push(linkData);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
catch (e) {
|
|
1047
|
-
linkData.category = 'invalid';
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
results.processed.push(linkData);
|
|
1051
|
-
});
|
|
1052
|
-
return results;
|
|
1053
|
-
}, processType);
|
|
1054
|
-
return {
|
|
1055
|
-
content: [{
|
|
1056
|
-
type: 'text',
|
|
1057
|
-
text: `✅ Links Processed\n\nTotal: ${processedLinks.processed.length}\nVideo: ${processedLinks.categorized.video.length}\nDownload: ${processedLinks.categorized.download.length}\n\n${JSON.stringify(processedLinks, null, 2)}`,
|
|
1058
|
-
}],
|
|
1059
|
-
};
|
|
1060
|
-
}, 'Failed to process links');
|
|
159
|
+
return { content: [{ type: 'text', text: "Link Process Extracts (Stub)" }] };
|
|
1061
160
|
}
|
|
1062
|
-
/**
|
|
1063
|
-
* Video Link Finders Extracts - Extract video links with metadata
|
|
1064
|
-
*/
|
|
1065
161
|
export async function handleVideoLinkFindersExtracts(args) {
|
|
1066
|
-
return
|
|
1067
|
-
validateWorkflow('video_link_finders_extracts', {
|
|
1068
|
-
requireBrowser: true,
|
|
1069
|
-
requirePage: true,
|
|
1070
|
-
});
|
|
1071
|
-
const page = getCurrentPage();
|
|
1072
|
-
const extracted = await page.evaluate(() => {
|
|
1073
|
-
const results = [];
|
|
1074
|
-
// Method 1: Direct video links
|
|
1075
|
-
document.querySelectorAll('a[href]').forEach((link) => {
|
|
1076
|
-
const href = link.href.toLowerCase();
|
|
1077
|
-
if (href.includes('.mp4') || href.includes('.webm')) {
|
|
1078
|
-
results.push({
|
|
1079
|
-
method: 'direct_link',
|
|
1080
|
-
url: link.href,
|
|
1081
|
-
text: link.textContent?.trim(),
|
|
1082
|
-
quality: link.dataset.quality || 'unknown',
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
});
|
|
1086
|
-
// Method 2: Video elements
|
|
1087
|
-
document.querySelectorAll('video').forEach((video) => {
|
|
1088
|
-
if (video.src) {
|
|
1089
|
-
results.push({
|
|
1090
|
-
method: 'video_element',
|
|
1091
|
-
url: video.src,
|
|
1092
|
-
poster: video.poster,
|
|
1093
|
-
duration: video.duration,
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
video.querySelectorAll('source').forEach((source) => {
|
|
1097
|
-
results.push({
|
|
1098
|
-
method: 'source_element',
|
|
1099
|
-
url: source.src,
|
|
1100
|
-
type: source.type,
|
|
1101
|
-
quality: source.dataset.quality || source.dataset.res || 'unknown',
|
|
1102
|
-
});
|
|
1103
|
-
});
|
|
1104
|
-
});
|
|
1105
|
-
// Method 3: Data attributes
|
|
1106
|
-
document.querySelectorAll('[data-video], [data-src]').forEach((el) => {
|
|
1107
|
-
const videoUrl = el.dataset.video || el.dataset.src;
|
|
1108
|
-
if (videoUrl) {
|
|
1109
|
-
results.push({
|
|
1110
|
-
method: 'data_attribute',
|
|
1111
|
-
url: videoUrl,
|
|
1112
|
-
element: el.tagName,
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
});
|
|
1116
|
-
return results;
|
|
1117
|
-
});
|
|
1118
|
-
return {
|
|
1119
|
-
content: [{
|
|
1120
|
-
type: 'text',
|
|
1121
|
-
text: `✅ Video Links Extracted: ${extracted.length}\n\n${JSON.stringify(extracted, null, 2)}`,
|
|
1122
|
-
}],
|
|
1123
|
-
};
|
|
1124
|
-
}, 'Failed to extract video links');
|
|
162
|
+
return handleVideoDownloadLinkFinder(args);
|
|
1125
163
|
}
|
|
1126
|
-
/**
|
|
1127
|
-
* Video Download Button Finders - Find all video download buttons
|
|
1128
|
-
*/
|
|
1129
164
|
export async function handleVideoDownloadButtonFinders(args) {
|
|
1130
|
-
return
|
|
1131
|
-
validateWorkflow('video_download_button_finders', {
|
|
1132
|
-
requireBrowser: true,
|
|
1133
|
-
requirePage: true,
|
|
1134
|
-
});
|
|
1135
|
-
const page = getCurrentPage();
|
|
1136
|
-
const downloadButtons = await page.evaluate(() => {
|
|
1137
|
-
const results = [];
|
|
1138
|
-
const foundElements = new Set(); // Avoid duplicates
|
|
1139
|
-
// Enhanced patterns for download buttons
|
|
1140
|
-
const buttonPatterns = [
|
|
1141
|
-
// Direct download attributes
|
|
1142
|
-
'a[download]',
|
|
1143
|
-
'button[download]',
|
|
1144
|
-
'[data-download]',
|
|
1145
|
-
'[data-download-url]',
|
|
1146
|
-
'[data-file]',
|
|
1147
|
-
'[data-link]',
|
|
1148
|
-
// Class-based
|
|
1149
|
-
'a[class*="download"]',
|
|
1150
|
-
'button[class*="download"]',
|
|
1151
|
-
'a[class*="btn-download"]',
|
|
1152
|
-
'[class*="download-button"]',
|
|
1153
|
-
'[class*="download-link"]',
|
|
1154
|
-
'[class*="dlvideoLinks"]',
|
|
1155
|
-
'[class*="btn-info"]',
|
|
1156
|
-
'[class*="btn-primary"]',
|
|
1157
|
-
// ID-based
|
|
1158
|
-
'[id*="download"]',
|
|
1159
|
-
'[id*="btn-download"]',
|
|
1160
|
-
'[id*="downloadButton"]',
|
|
1161
|
-
'[id*="Download"]',
|
|
1162
|
-
// Href patterns
|
|
1163
|
-
'a[href*="download"]',
|
|
1164
|
-
'a[href*=".mp4"]',
|
|
1165
|
-
'a[href*=".webm"]',
|
|
1166
|
-
'a[href*=".mkv"]',
|
|
1167
|
-
'a[href*=".avi"]',
|
|
1168
|
-
'a[href*="/file/"]',
|
|
1169
|
-
'a[href*="/stream/"]',
|
|
1170
|
-
'a[href*="ddn."]',
|
|
1171
|
-
'a[href*="igx."]',
|
|
1172
|
-
// Onclick patterns
|
|
1173
|
-
'[onclick*="download"]',
|
|
1174
|
-
'[onclick*="Download"]',
|
|
1175
|
-
'[onclick*="window.open"]',
|
|
1176
|
-
// Icon-based (common patterns)
|
|
1177
|
-
'a i[class*="download"]',
|
|
1178
|
-
'button i[class*="download"]',
|
|
1179
|
-
'.fa-download',
|
|
1180
|
-
'.icon-download',
|
|
1181
|
-
// Form submit buttons
|
|
1182
|
-
'input[type="submit"][value*="Download"]',
|
|
1183
|
-
'input[type="submit"][value*="Stream"]',
|
|
1184
|
-
'button[type="submit"]',
|
|
1185
|
-
];
|
|
1186
|
-
// Text-based search (case insensitive) - ENHANCED with GDL patterns
|
|
1187
|
-
const searchTexts = [
|
|
1188
|
-
'download', 'descargar', 'télécharger', 'baixar', 'скачать', 'save', 'get',
|
|
1189
|
-
'gdl', '5gdl', '4gdl', '3gdl', '2gdl', '1gdl', '⚡5gdl', // GDL variations with lightning
|
|
1190
|
-
'dl', 'down', 'grab', 'fetch', 'stream', 'watch', 'play', 'click'
|
|
1191
|
-
];
|
|
1192
|
-
buttonPatterns.forEach(pattern => {
|
|
1193
|
-
try {
|
|
1194
|
-
document.querySelectorAll(pattern).forEach((btn) => {
|
|
1195
|
-
// Avoid duplicates
|
|
1196
|
-
if (foundElements.has(btn))
|
|
1197
|
-
return;
|
|
1198
|
-
foundElements.add(btn);
|
|
1199
|
-
const isVisible = btn.offsetWidth > 0 && btn.offsetHeight > 0;
|
|
1200
|
-
const text = btn.textContent?.trim() || '';
|
|
1201
|
-
const href = btn.href || btn.getAttribute('href') || '';
|
|
1202
|
-
results.push({
|
|
1203
|
-
pattern,
|
|
1204
|
-
text,
|
|
1205
|
-
href,
|
|
1206
|
-
dataDownload: btn.dataset.download || btn.getAttribute('data-download'),
|
|
1207
|
-
isVisible,
|
|
1208
|
-
tag: btn.tagName.toLowerCase(),
|
|
1209
|
-
className: btn.className,
|
|
1210
|
-
id: btn.id,
|
|
1211
|
-
hasDownloadAttr: btn.hasAttribute('download'),
|
|
1212
|
-
onclick: btn.onclick ? 'present' : 'none'
|
|
1213
|
-
});
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
catch (e) {
|
|
1217
|
-
// Pattern not supported or error
|
|
1218
|
-
}
|
|
1219
|
-
});
|
|
1220
|
-
// Additional: Search for buttons/links with download-related text
|
|
1221
|
-
document.querySelectorAll('a, button').forEach((el) => {
|
|
1222
|
-
if (foundElements.has(el))
|
|
1223
|
-
return;
|
|
1224
|
-
const text = el.textContent?.toLowerCase() || '';
|
|
1225
|
-
const hasDownloadText = searchTexts.some(term => text.includes(term));
|
|
1226
|
-
if (hasDownloadText) {
|
|
1227
|
-
foundElements.add(el);
|
|
1228
|
-
const isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;
|
|
1229
|
-
results.push({
|
|
1230
|
-
pattern: 'text-based-search',
|
|
1231
|
-
text: el.textContent?.trim() || '',
|
|
1232
|
-
href: el.href || el.getAttribute('href') || '',
|
|
1233
|
-
dataDownload: el.dataset.download,
|
|
1234
|
-
isVisible,
|
|
1235
|
-
tag: el.tagName.toLowerCase(),
|
|
1236
|
-
className: el.className,
|
|
1237
|
-
id: el.id,
|
|
1238
|
-
matchedText: searchTexts.find(term => text.includes(term))
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1241
|
-
});
|
|
1242
|
-
return results;
|
|
1243
|
-
});
|
|
1244
|
-
// Include option to filter by visibility
|
|
1245
|
-
const includeHidden = args.includeHidden !== false; // Default: include hidden
|
|
1246
|
-
const filteredButtons = includeHidden ? downloadButtons : downloadButtons.filter((btn) => btn.isVisible);
|
|
1247
|
-
// If no buttons found, provide helpful context
|
|
1248
|
-
let additionalInfo = '';
|
|
1249
|
-
if (filteredButtons.length === 0) {
|
|
1250
|
-
const pageContext = await page.evaluate(() => {
|
|
1251
|
-
const allButtons = document.querySelectorAll('button, input[type="submit"], [role="button"]');
|
|
1252
|
-
const allLinks = document.querySelectorAll('a[href]');
|
|
1253
|
-
const allForms = document.querySelectorAll('form');
|
|
1254
|
-
return {
|
|
1255
|
-
totalButtons: allButtons.length,
|
|
1256
|
-
totalLinks: allLinks.length,
|
|
1257
|
-
totalForms: allForms.length,
|
|
1258
|
-
sampleButtons: Array.from(allButtons).slice(0, 5).map((b) => ({
|
|
1259
|
-
text: b.textContent?.trim().substring(0, 50) || '',
|
|
1260
|
-
id: b.id,
|
|
1261
|
-
className: b.className
|
|
1262
|
-
})),
|
|
1263
|
-
sampleLinks: Array.from(allLinks).slice(0, 5).map((l) => ({
|
|
1264
|
-
text: l.textContent?.trim().substring(0, 50) || '',
|
|
1265
|
-
href: l.href?.substring(0, 100) || ''
|
|
1266
|
-
}))
|
|
1267
|
-
};
|
|
1268
|
-
});
|
|
1269
|
-
additionalInfo = `\n\n💡 No download buttons found. Page has:\n • ${pageContext.totalButtons} buttons\n • ${pageContext.totalLinks} links\n • ${pageContext.totalForms} forms\n\nSample elements:\n${JSON.stringify(pageContext, null, 2)}`;
|
|
1270
|
-
}
|
|
1271
|
-
return {
|
|
1272
|
-
content: [{
|
|
1273
|
-
type: 'text',
|
|
1274
|
-
text: `✅ Found ${filteredButtons.length} download buttons (${downloadButtons.length} total, ${downloadButtons.filter((b) => !b.isVisible).length} hidden)\n\n${JSON.stringify(filteredButtons, null, 2)}${additionalInfo}`,
|
|
1275
|
-
}],
|
|
1276
|
-
};
|
|
1277
|
-
}, 'Failed to find download buttons');
|
|
165
|
+
return handleVideoDownloadPage(args);
|
|
1278
166
|
}
|