brave-real-browser-mcp-server 2.12.8 → 2.12.10
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.
|
@@ -275,32 +275,97 @@ export async function handleVideoPlayPushSource(args) {
|
|
|
275
275
|
}
|
|
276
276
|
};
|
|
277
277
|
page.on('response', responseHandler);
|
|
278
|
-
//
|
|
278
|
+
// Enhanced play button selectors
|
|
279
279
|
const playSelectors = [
|
|
280
280
|
'button[class*="play"]',
|
|
281
281
|
'[class*="play-button"]',
|
|
282
|
+
'[class*="btn-play"]',
|
|
282
283
|
'[aria-label*="Play"]',
|
|
284
|
+
'[aria-label*="play"]',
|
|
285
|
+
'button[title*="Play"]',
|
|
286
|
+
'button[title*="play"]',
|
|
283
287
|
'.video-play',
|
|
284
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"]',
|
|
285
296
|
];
|
|
286
297
|
let clicked = false;
|
|
298
|
+
let clickMethod = 'none';
|
|
299
|
+
// Try clicking play buttons
|
|
287
300
|
for (const selector of playSelectors) {
|
|
288
301
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
}
|
|
292
331
|
}
|
|
293
332
|
catch (e) {
|
|
294
333
|
// Try next selector
|
|
295
334
|
}
|
|
296
335
|
}
|
|
297
|
-
//
|
|
298
|
-
|
|
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);
|
|
299
364
|
page.off('response', responseHandler);
|
|
300
365
|
return {
|
|
301
366
|
content: [{
|
|
302
367
|
type: 'text',
|
|
303
|
-
text: `✅ Video sources captured\n\
|
|
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.'}`,
|
|
304
369
|
}],
|
|
305
370
|
};
|
|
306
371
|
}, 'Failed to capture video play sources');
|
|
@@ -316,46 +381,119 @@ export async function handleVideoPlayButtonClick(args) {
|
|
|
316
381
|
});
|
|
317
382
|
const page = getCurrentPage();
|
|
318
383
|
const customSelector = args.selector;
|
|
319
|
-
|
|
384
|
+
// Enhanced play button selectors
|
|
385
|
+
const defaultSelectors = [
|
|
320
386
|
'button[class*="play"]',
|
|
321
387
|
'[class*="play-button"]',
|
|
388
|
+
'[class*="btn-play"]',
|
|
322
389
|
'[aria-label*="Play"]',
|
|
390
|
+
'[aria-label*="play"]',
|
|
323
391
|
'button[title*="Play"]',
|
|
392
|
+
'button[title*="play"]',
|
|
324
393
|
'.video-play',
|
|
394
|
+
'.play-btn',
|
|
325
395
|
'#play-button',
|
|
326
|
-
'
|
|
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',
|
|
327
404
|
];
|
|
405
|
+
const playSelectors = customSelector ? [customSelector] : defaultSelectors;
|
|
406
|
+
const results = {
|
|
407
|
+
attempted: [],
|
|
408
|
+
clicked: false,
|
|
409
|
+
method: 'none',
|
|
410
|
+
selector: null
|
|
411
|
+
};
|
|
328
412
|
for (const selector of playSelectors) {
|
|
329
413
|
try {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 {
|
|
337
421
|
video.play();
|
|
422
|
+
success = true;
|
|
423
|
+
}
|
|
424
|
+
catch (e) { }
|
|
338
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
|
+
};
|
|
339
439
|
}
|
|
340
|
-
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
const element = await page.$(selector);
|
|
443
|
+
results.attempted.push({ selector, found: !!element });
|
|
444
|
+
if (element) {
|
|
341
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
|
+
};
|
|
342
455
|
}
|
|
343
|
-
return {
|
|
344
|
-
content: [{
|
|
345
|
-
type: 'text',
|
|
346
|
-
text: `✅ Play button clicked: ${selector}`,
|
|
347
|
-
}],
|
|
348
|
-
};
|
|
349
456
|
}
|
|
350
457
|
}
|
|
351
458
|
catch (e) {
|
|
352
|
-
|
|
459
|
+
results.attempted.push({ selector, error: String(e) });
|
|
353
460
|
}
|
|
354
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
|
+
}
|
|
355
493
|
return {
|
|
356
494
|
content: [{
|
|
357
495
|
type: 'text',
|
|
358
|
-
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')}`,
|
|
359
497
|
}],
|
|
360
498
|
};
|
|
361
499
|
}, 'Failed to click play button');
|
|
@@ -449,13 +587,17 @@ export async function handleNetworkRecordingFinder(args) {
|
|
|
449
587
|
const isStreamAsset = /\.m3u8(\?|$)|\.mpd(\?|$)|\.ts(\?|$)|\.vtt(\?|$)|\.mp4(\?|$)|\.webm(\?|$)/i.test(urlLower) ||
|
|
450
588
|
contentType.includes('application/vnd.apple.mpegurl') ||
|
|
451
589
|
contentType.includes('application/x-mpegurl');
|
|
452
|
-
|
|
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)) {
|
|
453
595
|
shouldRecord = true;
|
|
454
596
|
}
|
|
455
597
|
else if (filterType === 'audio' && contentType.includes('audio')) {
|
|
456
598
|
shouldRecord = true;
|
|
457
599
|
}
|
|
458
|
-
else if (filterType === 'media' && (contentType.includes('video') || contentType.includes('audio') || isStreamAsset)) {
|
|
600
|
+
else if (filterType === 'media' && (contentType.includes('video') || contentType.includes('audio') || isStreamAsset || isVideoAPI)) {
|
|
459
601
|
shouldRecord = true;
|
|
460
602
|
}
|
|
461
603
|
if (shouldRecord) {
|
|
@@ -556,19 +698,32 @@ export async function handleNetworkRecordingExtractors(args) {
|
|
|
556
698
|
apis: [],
|
|
557
699
|
};
|
|
558
700
|
let totalResponses = 0;
|
|
559
|
-
const responseHandler =
|
|
560
|
-
totalResponses++;
|
|
561
|
-
const url = response.url();
|
|
562
|
-
const contentType = response.headers()['content-type'] || '';
|
|
701
|
+
const responseHandler = (response) => {
|
|
563
702
|
try {
|
|
564
|
-
|
|
565
|
-
|
|
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) {
|
|
566
719
|
if (verbose)
|
|
567
720
|
console.log(`[Extractor] 🎥 Video found: ${url.substring(0, 80)}`);
|
|
568
721
|
extractedData.videos.push({
|
|
569
722
|
url,
|
|
570
723
|
contentType,
|
|
571
724
|
size: response.headers()['content-length'],
|
|
725
|
+
status: response.status(),
|
|
726
|
+
type: isVideoAPI ? 'api' : 'direct',
|
|
572
727
|
});
|
|
573
728
|
}
|
|
574
729
|
// Audio files
|
|
@@ -580,30 +735,31 @@ export async function handleNetworkRecordingExtractors(args) {
|
|
|
580
735
|
contentType,
|
|
581
736
|
});
|
|
582
737
|
}
|
|
583
|
-
// Manifest files (HLS, DASH)
|
|
738
|
+
// Manifest files (HLS, DASH) - Don't try to read content in handler
|
|
584
739
|
if (url.includes('.m3u8') || url.includes('.mpd')) {
|
|
585
740
|
if (verbose)
|
|
586
741
|
console.log(`[Extractor] 📜 Manifest found: ${url.substring(0, 80)}`);
|
|
587
|
-
const text = await response.text();
|
|
588
742
|
extractedData.manifests.push({
|
|
589
743
|
url,
|
|
590
744
|
type: url.includes('.m3u8') ? 'HLS' : 'DASH',
|
|
591
|
-
|
|
745
|
+
contentType,
|
|
746
|
+
status: response.status(),
|
|
592
747
|
});
|
|
593
748
|
}
|
|
594
|
-
// API responses with video data
|
|
595
|
-
if (contentType.includes('json') && (url.includes('video') || url.includes('media'))) {
|
|
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'))) {
|
|
596
751
|
if (verbose)
|
|
597
752
|
console.log(`[Extractor] 📡 API found: ${url.substring(0, 80)}`);
|
|
598
|
-
const json = await response.json();
|
|
599
753
|
extractedData.apis.push({
|
|
600
754
|
url,
|
|
601
|
-
|
|
755
|
+
contentType,
|
|
756
|
+
status: response.status(),
|
|
602
757
|
});
|
|
603
758
|
}
|
|
604
759
|
}
|
|
605
760
|
catch (e) {
|
|
606
|
-
|
|
761
|
+
if (verbose)
|
|
762
|
+
console.log(`[Extractor] ⚠️ Error processing response: ${e}`);
|
|
607
763
|
}
|
|
608
764
|
};
|
|
609
765
|
console.log(`[Extractor] 🎬 Starting extraction (${duration}ms)${navigateTo ? ` + navigating to ${navigateTo}` : ''}`);
|
|
@@ -768,6 +924,7 @@ export async function handleVideosSelectors(args) {
|
|
|
768
924
|
const selectors = await page.evaluate(() => {
|
|
769
925
|
const results = {
|
|
770
926
|
videoElements: [],
|
|
927
|
+
iframeElements: [],
|
|
771
928
|
playerContainers: [],
|
|
772
929
|
controlButtons: [],
|
|
773
930
|
sources: [],
|
|
@@ -781,15 +938,36 @@ export async function handleVideosSelectors(args) {
|
|
|
781
938
|
selector,
|
|
782
939
|
src: video.src,
|
|
783
940
|
hasControls: video.controls,
|
|
941
|
+
type: 'direct_video'
|
|
784
942
|
});
|
|
785
943
|
});
|
|
786
|
-
//
|
|
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)
|
|
787
960
|
['[class*="player"]', '[id*="player"]', '[data-player]'].forEach(sel => {
|
|
788
961
|
document.querySelectorAll(sel).forEach((el) => {
|
|
962
|
+
const hasVideo = !!el.querySelector('video');
|
|
963
|
+
const hasIframe = !!el.querySelector('iframe');
|
|
789
964
|
results.playerContainers.push({
|
|
790
965
|
selector: sel,
|
|
791
966
|
id: el.id,
|
|
792
967
|
className: el.className,
|
|
968
|
+
hasVideo,
|
|
969
|
+
hasIframe,
|
|
970
|
+
contentType: hasVideo ? 'video' : hasIframe ? 'iframe' : 'empty'
|
|
793
971
|
});
|
|
794
972
|
});
|
|
795
973
|
});
|
|
@@ -957,43 +1135,143 @@ export async function handleVideoDownloadButtonFinders(args) {
|
|
|
957
1135
|
const page = getCurrentPage();
|
|
958
1136
|
const downloadButtons = await page.evaluate(() => {
|
|
959
1137
|
const results = [];
|
|
1138
|
+
const foundElements = new Set(); // Avoid duplicates
|
|
1139
|
+
// Enhanced patterns for download buttons
|
|
960
1140
|
const buttonPatterns = [
|
|
961
|
-
|
|
962
|
-
'a[
|
|
1141
|
+
// Direct download attributes
|
|
1142
|
+
'a[download]',
|
|
1143
|
+
'button[download]',
|
|
963
1144
|
'[data-download]',
|
|
964
|
-
'
|
|
965
|
-
'
|
|
966
|
-
'
|
|
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
|
|
967
1173
|
'[onclick*="download"]',
|
|
968
|
-
'[
|
|
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'
|
|
969
1191
|
];
|
|
970
1192
|
buttonPatterns.forEach(pattern => {
|
|
971
1193
|
try {
|
|
972
|
-
document.querySelectorAll(pattern).forEach((btn
|
|
1194
|
+
document.querySelectorAll(pattern).forEach((btn) => {
|
|
1195
|
+
// Avoid duplicates
|
|
1196
|
+
if (foundElements.has(btn))
|
|
1197
|
+
return;
|
|
1198
|
+
foundElements.add(btn);
|
|
973
1199
|
const isVisible = btn.offsetWidth > 0 && btn.offsetHeight > 0;
|
|
1200
|
+
const text = btn.textContent?.trim() || '';
|
|
1201
|
+
const href = btn.href || btn.getAttribute('href') || '';
|
|
974
1202
|
results.push({
|
|
975
1203
|
pattern,
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
dataDownload: btn.dataset.download,
|
|
1204
|
+
text,
|
|
1205
|
+
href,
|
|
1206
|
+
dataDownload: btn.dataset.download || btn.getAttribute('data-download'),
|
|
980
1207
|
isVisible,
|
|
981
1208
|
tag: btn.tagName.toLowerCase(),
|
|
982
1209
|
className: btn.className,
|
|
983
1210
|
id: btn.id,
|
|
1211
|
+
hasDownloadAttr: btn.hasAttribute('download'),
|
|
1212
|
+
onclick: btn.onclick ? 'present' : 'none'
|
|
984
1213
|
});
|
|
985
1214
|
});
|
|
986
1215
|
}
|
|
987
1216
|
catch (e) {
|
|
988
|
-
// Pattern not supported
|
|
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
|
+
});
|
|
989
1240
|
}
|
|
990
1241
|
});
|
|
991
1242
|
return results;
|
|
992
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
|
+
}
|
|
993
1271
|
return {
|
|
994
1272
|
content: [{
|
|
995
1273
|
type: 'text',
|
|
996
|
-
text: `✅ Found ${
|
|
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}`,
|
|
997
1275
|
}],
|
|
998
1276
|
};
|
|
999
1277
|
}, 'Failed to find download buttons');
|
|
@@ -175,7 +175,7 @@ export async function handleXpathLinks(args) {
|
|
|
175
175
|
}, 'Failed to find XPath links');
|
|
176
176
|
}
|
|
177
177
|
/**
|
|
178
|
-
* AJAX Extractor - Extract AJAX/XHR request data
|
|
178
|
+
* AJAX Extractor - Extract AJAX/XHR request data with responses
|
|
179
179
|
*/
|
|
180
180
|
export async function handleAjaxExtractor(args) {
|
|
181
181
|
return await withErrorHandling(async () => {
|
|
@@ -186,29 +186,112 @@ export async function handleAjaxExtractor(args) {
|
|
|
186
186
|
const page = getCurrentPage();
|
|
187
187
|
const duration = args.duration || 15000;
|
|
188
188
|
const url = args.url;
|
|
189
|
+
const forceReload = args.forceReload !== false; // Force reload by default
|
|
190
|
+
const includeResponses = args.includeResponses !== false;
|
|
189
191
|
const requests = [];
|
|
192
|
+
const responses = [];
|
|
190
193
|
const requestHandler = (request) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
try {
|
|
195
|
+
const resourceType = request.resourceType();
|
|
196
|
+
if (resourceType === 'xhr' || resourceType === 'fetch') {
|
|
197
|
+
requests.push({
|
|
198
|
+
url: request.url(),
|
|
199
|
+
method: request.method(),
|
|
200
|
+
resourceType,
|
|
201
|
+
headers: request.headers(),
|
|
202
|
+
postData: request.postData(),
|
|
203
|
+
timestamp: new Date().toISOString(),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
// Ignore errors
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const responseHandler = async (response) => {
|
|
212
|
+
try {
|
|
213
|
+
const resourceType = response.request().resourceType();
|
|
214
|
+
if (resourceType === 'xhr' || resourceType === 'fetch') {
|
|
215
|
+
let body = null;
|
|
216
|
+
if (includeResponses) {
|
|
217
|
+
try {
|
|
218
|
+
const text = await response.text();
|
|
219
|
+
// Try to parse as JSON
|
|
220
|
+
try {
|
|
221
|
+
body = JSON.parse(text);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
body = text.substring(0, 500); // First 500 chars if not JSON
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
228
|
+
}
|
|
229
|
+
responses.push({
|
|
230
|
+
url: response.url(),
|
|
231
|
+
status: response.status(),
|
|
232
|
+
statusText: response.statusText(),
|
|
233
|
+
headers: response.headers(),
|
|
234
|
+
body,
|
|
235
|
+
timestamp: new Date().toISOString(),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
// Ignore errors
|
|
200
241
|
}
|
|
201
242
|
};
|
|
202
243
|
page.on('request', requestHandler);
|
|
244
|
+
if (includeResponses) {
|
|
245
|
+
page.on('response', responseHandler);
|
|
246
|
+
}
|
|
203
247
|
if (url && page.url() !== url) {
|
|
204
|
-
await page.goto(url, { waitUntil: 'networkidle2' });
|
|
248
|
+
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
249
|
+
}
|
|
250
|
+
else if (forceReload && !url) {
|
|
251
|
+
// Force reload current page to capture AJAX requests
|
|
252
|
+
try {
|
|
253
|
+
await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
|
|
254
|
+
}
|
|
255
|
+
catch { }
|
|
256
|
+
}
|
|
257
|
+
// Trigger interactions to generate AJAX requests
|
|
258
|
+
try {
|
|
259
|
+
await page.evaluate(() => {
|
|
260
|
+
// Scroll to trigger lazy loading
|
|
261
|
+
window.scrollTo(0, document.body.scrollHeight / 2);
|
|
262
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
263
|
+
// Click visible buttons that might trigger AJAX
|
|
264
|
+
const clickableElements = document.querySelectorAll('button, [role="button"], .btn, [onclick]');
|
|
265
|
+
clickableElements.forEach((el) => {
|
|
266
|
+
if (el.offsetWidth > 0 && el.offsetHeight > 0) {
|
|
267
|
+
const text = el.textContent?.toLowerCase() || '';
|
|
268
|
+
// Click safe elements (avoid dangerous buttons like delete, remove, etc.)
|
|
269
|
+
if (text.includes('load') || text.includes('more') || text.includes('show')) {
|
|
270
|
+
try {
|
|
271
|
+
el.click();
|
|
272
|
+
}
|
|
273
|
+
catch { }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
});
|
|
205
278
|
}
|
|
279
|
+
catch { }
|
|
206
280
|
await sleep(duration);
|
|
207
281
|
page.off('request', requestHandler);
|
|
282
|
+
if (includeResponses) {
|
|
283
|
+
page.off('response', responseHandler);
|
|
284
|
+
}
|
|
285
|
+
const combined = {
|
|
286
|
+
totalRequests: requests.length,
|
|
287
|
+
totalResponses: responses.length,
|
|
288
|
+
requests: requests.slice(0, 50), // First 50
|
|
289
|
+
responses: responses.slice(0, 50), // First 50
|
|
290
|
+
};
|
|
208
291
|
return {
|
|
209
292
|
content: [{
|
|
210
293
|
type: 'text',
|
|
211
|
-
text: `✅ Captured ${requests.length} AJAX/XHR requests\n\n${JSON.stringify(
|
|
294
|
+
text: `✅ Captured ${requests.length} AJAX/XHR requests and ${responses.length} responses\n\n${JSON.stringify(combined, null, 2)}${requests.length === 0 ? '\n\n💡 Tip: Page may not have AJAX requests, or use {"forceReload": true} to capture from page load' : ''}`,
|
|
212
295
|
}],
|
|
213
296
|
};
|
|
214
297
|
}, 'Failed to extract AJAX requests');
|
|
@@ -269,40 +352,109 @@ export async function handleNetworkRecorder(args) {
|
|
|
269
352
|
const page = getCurrentPage();
|
|
270
353
|
const duration = args.duration || 20000;
|
|
271
354
|
const filterTypes = args.filterTypes || ['video', 'xhr', 'fetch', 'media'];
|
|
355
|
+
const navigateUrl = args.navigateUrl; // Optional: Navigate to URL to capture from start
|
|
356
|
+
const clearCache = args.clearCache || false;
|
|
357
|
+
const forceReload = args.forceReload !== false; // Force reload by default to capture events
|
|
272
358
|
const networkActivity = [];
|
|
273
359
|
const requestHandler = (request) => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
360
|
+
try {
|
|
361
|
+
const resourceType = request.resourceType();
|
|
362
|
+
if (filterTypes.includes('all') || filterTypes.includes(resourceType)) {
|
|
363
|
+
networkActivity.push({
|
|
364
|
+
type: 'request',
|
|
365
|
+
url: request.url(),
|
|
366
|
+
method: request.method(),
|
|
367
|
+
resourceType,
|
|
368
|
+
headers: request.headers(),
|
|
369
|
+
postData: request.postData(),
|
|
370
|
+
timestamp: new Date().toISOString(),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
// Ignore request errors
|
|
283
376
|
}
|
|
284
377
|
};
|
|
285
|
-
const responseHandler = (response) => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
378
|
+
const responseHandler = async (response) => {
|
|
379
|
+
try {
|
|
380
|
+
const resourceType = response.request().resourceType();
|
|
381
|
+
if (filterTypes.includes('all') || filterTypes.includes(resourceType)) {
|
|
382
|
+
const headers = response.headers();
|
|
383
|
+
networkActivity.push({
|
|
384
|
+
type: 'response',
|
|
385
|
+
url: response.url(),
|
|
386
|
+
status: response.status(),
|
|
387
|
+
statusText: response.statusText(),
|
|
388
|
+
resourceType,
|
|
389
|
+
contentType: headers['content-type'] || '',
|
|
390
|
+
contentLength: headers['content-length'] || '',
|
|
391
|
+
timestamp: new Date().toISOString(),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
// Ignore response errors
|
|
295
397
|
}
|
|
296
398
|
};
|
|
399
|
+
// Start monitoring FIRST
|
|
297
400
|
page.on('request', requestHandler);
|
|
298
401
|
page.on('response', responseHandler);
|
|
402
|
+
// Optional: Clear cache for fresh load
|
|
403
|
+
if (clearCache) {
|
|
404
|
+
try {
|
|
405
|
+
const client = await page.target().createCDPSession();
|
|
406
|
+
await client.send('Network.clearBrowserCache');
|
|
407
|
+
await client.detach();
|
|
408
|
+
}
|
|
409
|
+
catch { }
|
|
410
|
+
}
|
|
411
|
+
// Optional: Navigate to URL (capturing from start)
|
|
412
|
+
if (navigateUrl) {
|
|
413
|
+
try {
|
|
414
|
+
await page.goto(navigateUrl, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
// Continue monitoring even if navigation fails
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else if (forceReload && !navigateUrl) {
|
|
421
|
+
// Force reload current page to capture network events
|
|
422
|
+
const currentUrl = page.url();
|
|
423
|
+
try {
|
|
424
|
+
await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
// Continue monitoring even if reload fails
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Also trigger any lazy-loaded content by scrolling
|
|
431
|
+
try {
|
|
432
|
+
await page.evaluate(() => {
|
|
433
|
+
window.scrollTo(0, document.body.scrollHeight / 2);
|
|
434
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
435
|
+
window.scrollTo(0, 0);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
catch { }
|
|
299
439
|
await sleep(duration);
|
|
300
440
|
page.off('request', requestHandler);
|
|
301
441
|
page.off('response', responseHandler);
|
|
442
|
+
const summary = {
|
|
443
|
+
totalEvents: networkActivity.length,
|
|
444
|
+
requests: networkActivity.filter(e => e.type === 'request').length,
|
|
445
|
+
responses: networkActivity.filter(e => e.type === 'response').length,
|
|
446
|
+
byResourceType: networkActivity.reduce((acc, e) => {
|
|
447
|
+
acc[e.resourceType] = (acc[e.resourceType] || 0) + 1;
|
|
448
|
+
return acc;
|
|
449
|
+
}, {})
|
|
450
|
+
};
|
|
451
|
+
const tipMessage = networkActivity.length === 0 ?
|
|
452
|
+
`\n\n💡 Tips:\n • Page was already loaded. Use {"navigateUrl": "https://example.com"} to capture from start\n • Use {"filterTypes": ["all"]} to capture all network activity\n • Use {"clearCache": true} for fresh page load` :
|
|
453
|
+
'';
|
|
302
454
|
return {
|
|
303
455
|
content: [{
|
|
304
456
|
type: 'text',
|
|
305
|
-
text: `✅ Recorded ${networkActivity.length} network events\n\n${JSON.stringify(networkActivity.slice(0, 50), null, 2)}${networkActivity.length > 50 ? '\n\n... (showing first 50)' : ''}`,
|
|
457
|
+
text: `✅ Recorded ${networkActivity.length} network events\n\n📊 Summary:\n${JSON.stringify(summary, null, 2)}\n\nEvents (first 50):\n${JSON.stringify(networkActivity.slice(0, 50), null, 2)}${networkActivity.length > 50 ? '\n\n... (showing first 50 of ' + networkActivity.length + ')' : ''}${tipMessage}`,
|
|
306
458
|
}],
|
|
307
459
|
};
|
|
308
460
|
}, 'Failed to record network');
|
|
@@ -384,13 +536,58 @@ export async function handleRegexPatternFinder(args) {
|
|
|
384
536
|
}
|
|
385
537
|
const matches = await page.evaluate(({ pattern, flags }) => {
|
|
386
538
|
const regex = new RegExp(pattern, flags);
|
|
539
|
+
const results = [];
|
|
540
|
+
// 1. Search in body HTML
|
|
387
541
|
const html = document.body.innerHTML;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
542
|
+
Array.from(html.matchAll(regex)).forEach(match => {
|
|
543
|
+
results.push({
|
|
544
|
+
source: 'html',
|
|
545
|
+
match: match[0],
|
|
546
|
+
groups: match.slice(1),
|
|
547
|
+
index: match.index
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
// 2. Search in script tags
|
|
551
|
+
document.querySelectorAll('script').forEach((script, scriptIdx) => {
|
|
552
|
+
const content = script.textContent || '';
|
|
553
|
+
Array.from(content.matchAll(regex)).forEach(match => {
|
|
554
|
+
results.push({
|
|
555
|
+
source: `script[${scriptIdx}]`,
|
|
556
|
+
match: match[0],
|
|
557
|
+
groups: match.slice(1),
|
|
558
|
+
index: match.index,
|
|
559
|
+
scriptSrc: script.src || 'inline'
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
// 3. Search in element attributes (href, src, data-* etc.)
|
|
564
|
+
document.querySelectorAll('*').forEach((el) => {
|
|
565
|
+
['href', 'src', 'data-video', 'data-src', 'data-url'].forEach(attr => {
|
|
566
|
+
const value = el.getAttribute(attr);
|
|
567
|
+
if (value) {
|
|
568
|
+
Array.from(value.matchAll(regex)).forEach(match => {
|
|
569
|
+
results.push({
|
|
570
|
+
source: `attribute[${attr}]`,
|
|
571
|
+
match: match[0],
|
|
572
|
+
groups: match.slice(1),
|
|
573
|
+
element: el.tagName.toLowerCase(),
|
|
574
|
+
attribute: attr,
|
|
575
|
+
fullValue: value
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
// Dedupe and limit
|
|
582
|
+
const seen = new Set();
|
|
583
|
+
const unique = results.filter(r => {
|
|
584
|
+
const key = `${r.source}:${r.match}`;
|
|
585
|
+
if (seen.has(key))
|
|
586
|
+
return false;
|
|
587
|
+
seen.add(key);
|
|
588
|
+
return true;
|
|
589
|
+
});
|
|
590
|
+
return unique.slice(0, 100);
|
|
394
591
|
}, { pattern, flags });
|
|
395
592
|
return {
|
|
396
593
|
content: [{
|
|
@@ -523,10 +720,15 @@ export async function handleVideoSourceExtractor(args) {
|
|
|
523
720
|
});
|
|
524
721
|
const page = getCurrentPage();
|
|
525
722
|
const captureDuration = typeof args.captureDuration === 'number' ? args.captureDuration : 6000;
|
|
526
|
-
// DOM video elements
|
|
527
|
-
const
|
|
723
|
+
// DOM video elements + iframe detection
|
|
724
|
+
const videoData = await page.evaluate(() => {
|
|
725
|
+
const results = {
|
|
726
|
+
videos: [],
|
|
727
|
+
iframes: [],
|
|
728
|
+
embeddedPlayers: []
|
|
729
|
+
};
|
|
730
|
+
// 1. Direct video elements
|
|
528
731
|
const videoElements = document.querySelectorAll('video');
|
|
529
|
-
const results = [];
|
|
530
732
|
videoElements.forEach((video, idx) => {
|
|
531
733
|
const sources = [];
|
|
532
734
|
// Direct src
|
|
@@ -540,15 +742,49 @@ export async function handleVideoSourceExtractor(args) {
|
|
|
540
742
|
type: source.type || 'unknown',
|
|
541
743
|
});
|
|
542
744
|
});
|
|
543
|
-
results.push({
|
|
745
|
+
results.videos.push({
|
|
544
746
|
index: idx,
|
|
545
747
|
poster: video.poster || '',
|
|
546
748
|
sources,
|
|
547
749
|
duration: video.duration || 0,
|
|
548
750
|
width: video.videoWidth || video.width || 0,
|
|
549
751
|
height: video.videoHeight || video.height || 0,
|
|
752
|
+
type: 'direct_video'
|
|
550
753
|
});
|
|
551
754
|
});
|
|
755
|
+
// 2. Iframe video sources
|
|
756
|
+
const iframes = document.querySelectorAll('iframe');
|
|
757
|
+
iframes.forEach((iframe, idx) => {
|
|
758
|
+
if (iframe.src) {
|
|
759
|
+
results.iframes.push({
|
|
760
|
+
index: idx,
|
|
761
|
+
src: iframe.src,
|
|
762
|
+
title: iframe.title || '',
|
|
763
|
+
id: iframe.id,
|
|
764
|
+
className: iframe.className,
|
|
765
|
+
width: iframe.width,
|
|
766
|
+
height: iframe.height,
|
|
767
|
+
type: 'iframe_video'
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
// 3. Video players with iframes inside
|
|
772
|
+
const playerContainers = document.querySelectorAll('[class*="player"], [id*="player"], [data-player]');
|
|
773
|
+
playerContainers.forEach((container, idx) => {
|
|
774
|
+
const iframe = container.querySelector('iframe');
|
|
775
|
+
const video = container.querySelector('video');
|
|
776
|
+
if (iframe || video) {
|
|
777
|
+
results.embeddedPlayers.push({
|
|
778
|
+
index: idx,
|
|
779
|
+
hasVideo: !!video,
|
|
780
|
+
hasIframe: !!iframe,
|
|
781
|
+
videoSrc: video ? (video.src || video.currentSrc) : null,
|
|
782
|
+
iframeSrc: iframe ? iframe.src : null,
|
|
783
|
+
containerId: container.id,
|
|
784
|
+
containerClass: container.className
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
});
|
|
552
788
|
return results;
|
|
553
789
|
});
|
|
554
790
|
// Network capture for manifests and segments
|
|
@@ -584,11 +820,22 @@ export async function handleVideoSourceExtractor(args) {
|
|
|
584
820
|
catch { }
|
|
585
821
|
await sleep(captureDuration);
|
|
586
822
|
page.off('response', respHandler);
|
|
587
|
-
const result = {
|
|
823
|
+
const result = {
|
|
824
|
+
...videoData,
|
|
825
|
+
manifests,
|
|
826
|
+
segments,
|
|
827
|
+
summary: {
|
|
828
|
+
totalVideos: videoData.videos.length,
|
|
829
|
+
totalIframes: videoData.iframes.length,
|
|
830
|
+
totalEmbeddedPlayers: videoData.embeddedPlayers.length,
|
|
831
|
+
totalManifests: manifests.length,
|
|
832
|
+
totalSegments: segments.length
|
|
833
|
+
}
|
|
834
|
+
};
|
|
588
835
|
return {
|
|
589
836
|
content: [{
|
|
590
837
|
type: 'text',
|
|
591
|
-
text: `✅ Extracted video sources\n\n${JSON.stringify(result, null, 2)}`,
|
|
838
|
+
text: `✅ Extracted video sources\n\n📊 Summary:\n • Direct <video> elements: ${videoData.videos.length}\n • Iframe sources: ${videoData.iframes.length}\n • Embedded players: ${videoData.embeddedPlayers.length}\n • Manifests: ${manifests.length}\n • Segments: ${segments.length}\n\n${JSON.stringify(result, null, 2)}`,
|
|
592
839
|
}],
|
|
593
840
|
};
|
|
594
841
|
}, 'Failed to extract video sources');
|
|
@@ -614,20 +861,65 @@ export async function handleVideoPlayerExtractor(args) {
|
|
|
614
861
|
];
|
|
615
862
|
playerSelectors.forEach(selector => {
|
|
616
863
|
document.querySelectorAll(selector).forEach((el, idx) => {
|
|
617
|
-
const videoEl = el.querySelector('video')
|
|
618
|
-
|
|
619
|
-
|
|
864
|
+
const videoEl = el.querySelector('video');
|
|
865
|
+
const iframeEl = el.querySelector('iframe');
|
|
866
|
+
if (videoEl || iframeEl) {
|
|
867
|
+
const playerInfo = {
|
|
620
868
|
selector,
|
|
621
869
|
index: idx,
|
|
622
|
-
hasVideo: !!
|
|
623
|
-
hasIframe: !!
|
|
624
|
-
src: videoEl.src || videoEl.currentSrc || '',
|
|
870
|
+
hasVideo: !!videoEl,
|
|
871
|
+
hasIframe: !!iframeEl,
|
|
625
872
|
className: el.className,
|
|
626
873
|
id: el.id,
|
|
627
|
-
}
|
|
874
|
+
};
|
|
875
|
+
// Video element info
|
|
876
|
+
if (videoEl) {
|
|
877
|
+
playerInfo.videoSrc = videoEl.src || videoEl.currentSrc || '';
|
|
878
|
+
playerInfo.videoPoster = videoEl.poster || '';
|
|
879
|
+
playerInfo.videoType = 'direct';
|
|
880
|
+
}
|
|
881
|
+
// Iframe element info
|
|
882
|
+
if (iframeEl) {
|
|
883
|
+
playerInfo.iframeSrc = iframeEl.src || '';
|
|
884
|
+
playerInfo.iframeTitle = iframeEl.title || '';
|
|
885
|
+
playerInfo.iframeAllow = iframeEl.getAttribute('allow') || '';
|
|
886
|
+
playerInfo.videoType = videoEl ? 'hybrid' : 'iframe';
|
|
887
|
+
}
|
|
888
|
+
results.push(playerInfo);
|
|
628
889
|
}
|
|
629
890
|
});
|
|
630
891
|
});
|
|
892
|
+
// Also check standalone iframes (ALL iframes that might be video players)
|
|
893
|
+
document.querySelectorAll('iframe').forEach((iframe, idx) => {
|
|
894
|
+
const src = (iframe.src || '').toLowerCase();
|
|
895
|
+
// Check if iframe is likely a video player
|
|
896
|
+
const isLikelyVideoIframe = src.includes('embed') ||
|
|
897
|
+
src.includes('player') ||
|
|
898
|
+
src.includes('video') ||
|
|
899
|
+
src.includes('stream') ||
|
|
900
|
+
iframe.allow?.includes('autoplay') ||
|
|
901
|
+
iframe.allow?.includes('encrypted-media');
|
|
902
|
+
// Include ALL iframes if they have src and are likely video players
|
|
903
|
+
if (iframe.src && isLikelyVideoIframe) {
|
|
904
|
+
// Check if already added
|
|
905
|
+
const alreadyAdded = results.some(r => r.iframeSrc === iframe.src);
|
|
906
|
+
if (!alreadyAdded) {
|
|
907
|
+
results.push({
|
|
908
|
+
selector: iframe.id ? `#${iframe.id}` : `iframe:nth-of-type(${idx + 1})`,
|
|
909
|
+
index: idx,
|
|
910
|
+
hasVideo: false,
|
|
911
|
+
hasIframe: true,
|
|
912
|
+
iframeSrc: iframe.src,
|
|
913
|
+
iframeTitle: iframe.title || '',
|
|
914
|
+
iframeAllow: iframe.getAttribute('allow') || '',
|
|
915
|
+
className: iframe.className,
|
|
916
|
+
id: iframe.id,
|
|
917
|
+
videoType: 'standalone_iframe',
|
|
918
|
+
isVisible: iframe.offsetWidth > 0 && iframe.offsetHeight > 0
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
});
|
|
631
923
|
return results;
|
|
632
924
|
});
|
|
633
925
|
return {
|
|
@@ -652,6 +944,7 @@ export async function handleVideoPlayerHosterFinder(args) {
|
|
|
652
944
|
const results = [];
|
|
653
945
|
const iframes = document.querySelectorAll('iframe');
|
|
654
946
|
const platforms = {
|
|
947
|
+
// Popular platforms
|
|
655
948
|
'youtube.com': 'YouTube',
|
|
656
949
|
'youtu.be': 'YouTube',
|
|
657
950
|
'vimeo.com': 'Vimeo',
|
|
@@ -660,6 +953,23 @@ export async function handleVideoPlayerHosterFinder(args) {
|
|
|
660
953
|
'twitter.com': 'Twitter',
|
|
661
954
|
'twitch.tv': 'Twitch',
|
|
662
955
|
'streamable.com': 'Streamable',
|
|
956
|
+
// Custom video hosting platforms
|
|
957
|
+
'gdmirrorbot': 'GD Mirror Bot',
|
|
958
|
+
'multimoviesshg.com': 'MultiMovies StreamHG',
|
|
959
|
+
'streamhg.com': 'StreamHG',
|
|
960
|
+
'techinmind.space': 'Tech In Mind Player',
|
|
961
|
+
'premilkyway.com': 'Premium Milky Way CDN',
|
|
962
|
+
'p2pplay.pro': 'P2P Play',
|
|
963
|
+
'rpmhub.site': 'RPM Share',
|
|
964
|
+
'uns.bio': 'UpnShare',
|
|
965
|
+
'smoothpre.com': 'EarnVids/SmoothPre',
|
|
966
|
+
'doodstream.com': 'DoodStream',
|
|
967
|
+
'streamtape.com': 'StreamTape',
|
|
968
|
+
'mixdrop.co': 'MixDrop',
|
|
969
|
+
'upstream.to': 'UpStream',
|
|
970
|
+
'vidcloud': 'VidCloud',
|
|
971
|
+
'fembed': 'Fembed',
|
|
972
|
+
'mp4upload': 'MP4Upload',
|
|
663
973
|
};
|
|
664
974
|
iframes.forEach((iframe, idx) => {
|
|
665
975
|
const src = iframe.src.toLowerCase();
|
|
@@ -113,7 +113,17 @@ export class SseTransport {
|
|
|
113
113
|
error: errorMessage,
|
|
114
114
|
timestamp: new Date().toISOString(),
|
|
115
115
|
});
|
|
116
|
-
|
|
116
|
+
// Return errors in MCP format (status 200 with isError flag)
|
|
117
|
+
res.json({
|
|
118
|
+
success: true,
|
|
119
|
+
result: {
|
|
120
|
+
isError: true,
|
|
121
|
+
content: [{
|
|
122
|
+
type: 'text',
|
|
123
|
+
text: errorMessage
|
|
124
|
+
}]
|
|
125
|
+
}
|
|
126
|
+
});
|
|
117
127
|
}
|
|
118
128
|
});
|
|
119
129
|
// Browser automation endpoints with SSE notifications
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brave-real-browser-mcp-server",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.10",
|
|
4
4
|
"description": "Universal AI IDE MCP Server - Auto-detects and supports all AI IDEs (Claude Desktop, Cursor, Windsurf, Cline, Zed, VSCode, Qoder AI, etc.) with Brave browser automation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
40
|
-
"@types/turndown": "^5.0.
|
|
40
|
+
"@types/turndown": "^5.0.6",
|
|
41
41
|
"ajv": "^8.12.0",
|
|
42
42
|
"axios": "^1.6.5",
|
|
43
43
|
"brave-real-browser": "^1.5.102",
|
|
44
|
-
"brave-real-launcher": "^1.2.
|
|
44
|
+
"brave-real-launcher": "^1.2.19",
|
|
45
45
|
"brave-real-puppeteer-core": "^24.26.1",
|
|
46
46
|
"cheerio": "^1.0.0-rc.12",
|
|
47
47
|
"chrono-node": "^2.7.0",
|