mcp-scraper 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +6 -5
  2. package/dist/bin/api-server.cjs +321 -29
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +1 -1
  5. package/dist/bin/browser-agent-stdio-server.cjs +1 -1
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +2 -2
  8. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-cli.js +1 -1
  11. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +95 -6
  12. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-scraper-combined-stdio-server.js +3 -3
  14. package/dist/bin/mcp-scraper-install.cjs +4 -4
  15. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  16. package/dist/bin/mcp-scraper-install.js +4 -4
  17. package/dist/bin/mcp-scraper-install.js.map +1 -1
  18. package/dist/bin/mcp-stdio-server.cjs +95 -6
  19. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  20. package/dist/bin/mcp-stdio-server.js +2 -2
  21. package/dist/{chunk-ZV2XXYR7.js → chunk-ATJAINML.js} +2 -2
  22. package/dist/{chunk-IWQTNY2E.js → chunk-FMC3V54I.js} +96 -7
  23. package/dist/chunk-FMC3V54I.js.map +1 -0
  24. package/dist/chunk-KE4JZDLV.js +7 -0
  25. package/dist/chunk-KE4JZDLV.js.map +1 -0
  26. package/dist/{server-7QP6HQJJ.js → server-KSEQLZNP.js} +221 -24
  27. package/dist/server-KSEQLZNP.js.map +1 -0
  28. package/package.json +1 -1
  29. package/dist/chunk-IWQTNY2E.js.map +0 -1
  30. package/dist/chunk-ROCXJOVY.js +0 -7
  31. package/dist/chunk-ROCXJOVY.js.map +0 -1
  32. package/dist/server-7QP6HQJJ.js.map +0 -1
  33. /package/dist/{chunk-ZV2XXYR7.js.map → chunk-ATJAINML.js.map} +0 -0
@@ -100,7 +100,7 @@ import {
100
100
  harvestTimeoutBudget,
101
101
  liveWebToolAnnotations,
102
102
  outputBaseDir
103
- } from "./chunk-IWQTNY2E.js";
103
+ } from "./chunk-FMC3V54I.js";
104
104
  import {
105
105
  CaptchaError,
106
106
  RECAPTCHA_INSTRUCTIONS,
@@ -114,7 +114,7 @@ import {
114
114
  workflowDefinition,
115
115
  workflowOutputBaseDir
116
116
  } from "./chunk-F44RBOJ5.js";
117
- import "./chunk-ROCXJOVY.js";
117
+ import "./chunk-KE4JZDLV.js";
118
118
 
119
119
  // src/api/outbound-sanitize.ts
120
120
  var KEY_RENAMES = {
@@ -9189,6 +9189,127 @@ function advertisersFromResults(results, maxResults) {
9189
9189
  return [...byPage.values()].map((e) => ({ pageName: e.pageName, pageId: e.pageId, sampleLibraryId: e.sampleLibraryId, adCount: Math.max(e.maxCollation, e.resultCount) })).sort((a, b) => b.adCount - a.adCount).slice(0, maxResults);
9190
9190
  }
9191
9191
 
9192
+ // src/extractor/FacebookOrganicVideoExtractor.ts
9193
+ function htmlDecode(value) {
9194
+ return value.replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#039;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
9195
+ }
9196
+ function decodeEscapedValue(raw) {
9197
+ let out = raw;
9198
+ try {
9199
+ out = JSON.parse(`"${raw.replace(/"/g, '\\"')}"`);
9200
+ } catch {
9201
+ out = raw.replace(/\\u0025/g, "%").replace(/\\u0026/g, "&").replace(/\\u003D/g, "=").replace(/\\u003d/g, "=").replace(/\\"/g, '"').replace(/\\\//g, "/");
9202
+ }
9203
+ return htmlDecode(out);
9204
+ }
9205
+ function facebookVideoIdFromUrl(url) {
9206
+ try {
9207
+ const parsed = new URL(url);
9208
+ const path5 = parsed.pathname;
9209
+ const direct = path5.match(/\/(?:reel|videos|watch)\/(?:[^/]+\/)?(\d{8,})/i);
9210
+ if (direct) return direct[1];
9211
+ const queryVideoId = parsed.searchParams.get("v") ?? parsed.searchParams.get("video_id");
9212
+ if (queryVideoId && /^\d{8,}$/.test(queryVideoId)) return queryVideoId;
9213
+ const numericTail = path5.match(/\/(\d{8,})\/?$/);
9214
+ if (numericTail) return numericTail[1];
9215
+ } catch {
9216
+ }
9217
+ return null;
9218
+ }
9219
+ function parseEfg(url) {
9220
+ try {
9221
+ const efg = new URL(url).searchParams.get("efg");
9222
+ if (!efg) return null;
9223
+ return JSON.parse(Buffer.from(efg, "base64").toString("utf8"));
9224
+ } catch {
9225
+ return null;
9226
+ }
9227
+ }
9228
+ function manifestDurationSec(htmlWindow) {
9229
+ const decoded = decodeEscapedValue(htmlWindow);
9230
+ const match = decoded.match(/mediaPresentationDuration="PT([\d.]+)S"/);
9231
+ return match ? Number(match[1]) : null;
9232
+ }
9233
+ function metadataContent(html, key) {
9234
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9235
+ const re = new RegExp(`<meta\\s+[^>]*(?:property|name)=["']${escaped}["'][^>]*content=["']([^"']+)["'][^>]*>`, "i");
9236
+ const m = html.match(re);
9237
+ return m ? htmlDecode(m[1]).trim() : null;
9238
+ }
9239
+ function ownerNameFromHtml(html) {
9240
+ const ogTitle = metadataContent(html, "og:title");
9241
+ const pipe = ogTitle?.split("|").map((s) => s.trim()).filter(Boolean);
9242
+ if (pipe && pipe.length > 1) return pipe[pipe.length - 1] ?? null;
9243
+ return null;
9244
+ }
9245
+ function extractFacebookOrganicVideo(html, pageUrl, sourceUrl = pageUrl, quality = "best") {
9246
+ const videoId = facebookVideoIdFromUrl(pageUrl) ?? facebookVideoIdFromUrl(sourceUrl);
9247
+ const manifestIndex = videoId ? html.indexOf(`dash_mpd_debug.mpd?v=${videoId}`) : -1;
9248
+ const searchWindow = manifestIndex >= 0 ? html.slice(Math.max(0, manifestIndex - 5e4), Math.min(html.length, manifestIndex + 5e4)) : html;
9249
+ const targetDuration = manifestDurationSec(searchWindow);
9250
+ const candidates = [];
9251
+ const seen = /* @__PURE__ */ new Set();
9252
+ const re = /browser_native_(sd|hd)_url\\?"\s*:\s*\\?"([^"<]+?)\\?"/g;
9253
+ let match;
9254
+ while ((match = re.exec(searchWindow)) !== null) {
9255
+ const url = decodeEscapedValue(match[2]);
9256
+ if (!url || seen.has(url)) continue;
9257
+ seen.add(url);
9258
+ const parsed = parseEfg(url);
9259
+ let bitrate = null;
9260
+ try {
9261
+ const value = new URL(url).searchParams.get("bitrate");
9262
+ bitrate = value ? Number(value) : null;
9263
+ } catch {
9264
+ }
9265
+ candidates.push({
9266
+ url,
9267
+ quality: match[1],
9268
+ bitrate: Number.isFinite(bitrate) ? bitrate : null,
9269
+ durationSec: typeof parsed?.duration_s === "number" ? parsed.duration_s : null,
9270
+ assetId: typeof parsed?.xpv_asset_id === "number" ? parsed.xpv_asset_id : null,
9271
+ vencodeTag: typeof parsed?.vencode_tag === "string" ? parsed.vencode_tag : null
9272
+ });
9273
+ }
9274
+ const targetCandidates = candidates.filter((candidate) => {
9275
+ if (!targetDuration || candidate.durationSec == null) return true;
9276
+ return Math.abs(candidate.durationSec - targetDuration) <= 2;
9277
+ });
9278
+ const selectable = targetCandidates.length ? targetCandidates : candidates;
9279
+ const selected = [...selectable].filter((candidate) => quality === "best" || candidate.quality === quality).sort((a, b) => {
9280
+ const aq = a.quality === "hd" ? 1 : 0;
9281
+ const bq = b.quality === "hd" ? 1 : 0;
9282
+ if (quality === "best" && aq !== bq) return bq - aq;
9283
+ return (b.bitrate ?? 0) - (a.bitrate ?? 0);
9284
+ })[0];
9285
+ if (!selected) {
9286
+ throw new Error("No progressive Facebook video URL was found in the rendered page state");
9287
+ }
9288
+ return {
9289
+ sourceUrl,
9290
+ pageUrl,
9291
+ videoId,
9292
+ title: metadataContent(html, "og:title") ?? metadataContent(html, "twitter:title"),
9293
+ description: metadataContent(html, "og:description") ?? metadataContent(html, "description") ?? metadataContent(html, "twitter:description"),
9294
+ ownerName: ownerNameFromHtml(html),
9295
+ videoUrl: selected.url,
9296
+ selectedQuality: selected.quality,
9297
+ bitrate: selected.bitrate,
9298
+ durationSec: targetDuration ?? selected.durationSec,
9299
+ assetId: selected.assetId,
9300
+ vencodeTag: selected.vencodeTag,
9301
+ candidates: selectable,
9302
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString()
9303
+ };
9304
+ }
9305
+ async function extractFacebookOrganicVideoFromPage(page, sourceUrl, quality = "best") {
9306
+ await page.waitForLoadState("domcontentloaded", { timeout: 15e3 }).catch(() => {
9307
+ });
9308
+ await page.waitForTimeout(1500);
9309
+ const html = await page.content();
9310
+ return extractFacebookOrganicVideo(html, page.url(), sourceUrl, quality);
9311
+ }
9312
+
9192
9313
  // src/api/facebook-ad-routes.ts
9193
9314
  import { fal as fal2 } from "@fal-ai/client";
9194
9315
  var FacebookAdBodySchema = z13.object({
@@ -9208,6 +9329,10 @@ var FacebookPageIntelBodySchema = z13.object({
9208
9329
  var FacebookTranscribeBodySchema = z13.object({
9209
9330
  videoUrl: z13.string().trim().min(1, "videoUrl is required")
9210
9331
  });
9332
+ var FacebookVideoTranscribeBodySchema = z13.object({
9333
+ url: z13.string().trim().min(1, "url is required"),
9334
+ quality: z13.enum(["best", "hd", "sd"]).default("best")
9335
+ });
9211
9336
  var FacebookSearchBodySchema = z13.object({
9212
9337
  query: z13.string().trim().min(1, "query is required"),
9213
9338
  country: z13.string().trim().toUpperCase().optional(),
@@ -9220,6 +9345,39 @@ var FacebookMediaBodySchema = z13.object({
9220
9345
  function invalidRequest(message) {
9221
9346
  return { error_code: "invalid_request", message };
9222
9347
  }
9348
+ function isAllowedFacebookOrganicUrl(url) {
9349
+ const hostname = url.hostname.toLowerCase().replace(/^www\./, "").replace(/^m\./, "");
9350
+ return hostname === "facebook.com" || hostname === "fb.watch";
9351
+ }
9352
+ function facebookTranscriptMarkdown(title, text, chunks, durationMs) {
9353
+ const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
9354
+ const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
9355
+ if (chunks.length) {
9356
+ lines.push("## Timestamped Segments", "");
9357
+ for (const ch of chunks) {
9358
+ lines.push(`**[${fmtTs2(ch.timestamp[0])} -> ${fmtTs2(ch.timestamp[1])}]** ${ch.text.trim()}`, "");
9359
+ }
9360
+ }
9361
+ return lines.join("\n");
9362
+ }
9363
+ async function transcribeFacebookVideoUrl(videoUrl, markdownTitle = "# Facebook Video Transcript") {
9364
+ const startMs = Date.now();
9365
+ const result = await fal2.subscribe("fal-ai/wizper", {
9366
+ input: { audio_url: videoUrl, task: "transcribe", language: "en" },
9367
+ logs: false,
9368
+ pollInterval: 3e3
9369
+ });
9370
+ const data = result.data;
9371
+ const text = data.text ?? "";
9372
+ const chunks = data.chunks ?? [];
9373
+ const durationMs = Date.now() - startMs;
9374
+ return {
9375
+ text,
9376
+ chunks,
9377
+ durationMs,
9378
+ markdown: facebookTranscriptMarkdown(markdownTitle, text, chunks, durationMs)
9379
+ };
9380
+ }
9223
9381
  async function detectSoftBlock(driver) {
9224
9382
  const page = driver.getPage();
9225
9383
  const bodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
@@ -9363,26 +9521,9 @@ facebookAdApp.post("/transcribe", createApiKeyAuth(), async (c) => {
9363
9521
  const { ok, balance_mc } = await debitMc(fbUser.id, MC_COSTS.fb_transcribe, LedgerOperation.FB_TRANSCRIBE, videoUrl);
9364
9522
  if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.fb_transcribe), 402);
9365
9523
  debited = true;
9366
- const startMs = Date.now();
9367
- const result = await fal2.subscribe("fal-ai/wizper", {
9368
- input: { audio_url: videoUrl, task: "transcribe", language: "en" },
9369
- logs: false,
9370
- pollInterval: 3e3
9371
- });
9372
- const data = result.data;
9373
- const text = data.text ?? "";
9374
- const chunks = data.chunks ?? [];
9375
- const durationMs = Date.now() - startMs;
9376
- const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
9377
- const lines = ["# Facebook Ad Transcript", "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
9378
- if (chunks.length) {
9379
- lines.push("## Timestamped Segments", "");
9380
- for (const ch of chunks) {
9381
- lines.push(`**[${fmtTs2(ch.timestamp[0])} \u2192 ${fmtTs2(ch.timestamp[1])}]** ${ch.text.trim()}`, "");
9382
- }
9383
- }
9384
- await logRequestEvent({ userId: fbUser.id, source: "facebook_transcribe", status: "done", query: videoUrl, resultCount: chunks.length, result: { text, chunks, durationMs } });
9385
- return c.json({ text, chunks, durationMs, markdown: lines.join("\n") });
9524
+ const transcript = await transcribeFacebookVideoUrl(videoUrl, "# Facebook Ad Transcript");
9525
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_transcribe", status: "done", query: videoUrl, resultCount: transcript.chunks.length, result: { text: transcript.text, chunks: transcript.chunks, durationMs: transcript.durationMs } });
9526
+ return c.json(transcript);
9386
9527
  } catch (err) {
9387
9528
  const msg = err instanceof Error ? err.message : String(err);
9388
9529
  if (debited) await creditMc(fbUser.id, MC_COSTS.fb_transcribe, LedgerOperation.FB_TRANSCRIBE_REFUND, "failed call");
@@ -9392,6 +9533,62 @@ facebookAdApp.post("/transcribe", createApiKeyAuth(), async (c) => {
9392
9533
  await releaseConcurrencyGate(gate.lockId);
9393
9534
  }
9394
9535
  });
9536
+ facebookAdApp.post("/video-transcribe", createApiKeyAuth(), async (c) => {
9537
+ const raw = await c.req.json().catch(() => ({}));
9538
+ const parsed = FacebookVideoTranscribeBodySchema.safeParse(raw);
9539
+ if (!parsed.success) {
9540
+ return c.json(invalidRequest(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
9541
+ }
9542
+ const body = parsed.data;
9543
+ const urlCheck = await validatePublicHttpUrl(body.url, { field: "url", requireHttps: false });
9544
+ if (urlCheck.error) {
9545
+ return c.json(invalidRequest(urlCheck.error), 400);
9546
+ }
9547
+ const sourceUrl = urlCheck.parsed;
9548
+ if (!isAllowedFacebookOrganicUrl(sourceUrl)) {
9549
+ return c.json(invalidRequest("url must be a Facebook organic video, reel, or share URL"), 400);
9550
+ }
9551
+ const fbUser = c.get("user");
9552
+ const gate = await acquireConcurrencyGate(fbUser, "facebook_video_transcribe", {
9553
+ reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
9554
+ metadata: { url: sourceUrl.href }
9555
+ });
9556
+ if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
9557
+ fal2.config({ credentials: process.env.FAL_KEY });
9558
+ const driver = new BrowserDriver();
9559
+ let debited = false;
9560
+ try {
9561
+ const { ok, balance_mc } = await debitMc(fbUser.id, MC_COSTS.fb_transcribe, LedgerOperation.FB_TRANSCRIBE, sourceUrl.href);
9562
+ if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.fb_transcribe), 402);
9563
+ debited = true;
9564
+ await driver.launch(await kernelLaunchOptsResidential());
9565
+ await driver.navigateTo(sourceUrl.href);
9566
+ const page = driver.getPage();
9567
+ const video = await extractFacebookOrganicVideoFromPage(page, sourceUrl.href, body.quality);
9568
+ const transcript = await transcribeFacebookVideoUrl(video.videoUrl, "# Facebook Organic Video Transcript");
9569
+ const result = {
9570
+ ...video,
9571
+ videoDurationSec: video.durationSec,
9572
+ text: transcript.text,
9573
+ chunks: transcript.chunks,
9574
+ durationMs: transcript.durationMs,
9575
+ markdown: transcript.markdown
9576
+ };
9577
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_video_transcribe", status: "done", query: sourceUrl.href, resultCount: transcript.chunks.length, result });
9578
+ return c.json(result);
9579
+ } catch (err) {
9580
+ const msg = err instanceof Error ? err.message : String(err);
9581
+ if (debited) await creditMc(fbUser.id, MC_COSTS.fb_transcribe, LedgerOperation.FB_TRANSCRIBE_REFUND, "failed call");
9582
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_video_transcribe", status: "failed", query: sourceUrl.href, error: msg });
9583
+ if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) {
9584
+ return c.json({ error: msg }, 503);
9585
+ }
9586
+ return c.json({ error: msg }, 500);
9587
+ } finally {
9588
+ await driver.close();
9589
+ await releaseConcurrencyGate(gate.lockId);
9590
+ }
9591
+ });
9395
9592
  facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
9396
9593
  const raw = await c.req.json().catch(() => ({}));
9397
9594
  const parsed = FacebookSearchBodySchema.safeParse(raw);
@@ -13602,7 +13799,7 @@ var sessionCookieOptions = {
13602
13799
  sameSite: "Strict"
13603
13800
  };
13604
13801
  function configuredOrigins() {
13605
- const origins = /* @__PURE__ */ new Set(["https://mcpscraper.dev"]);
13802
+ const origins = /* @__PURE__ */ new Set(["https://mcpscraper.dev", "https://www.mcpscraper.dev"]);
13606
13803
  for (const raw of (process.env.ALLOWED_ORIGINS ?? process.env.APP_ORIGIN ?? "").split(",")) {
13607
13804
  const trimmed = raw.trim();
13608
13805
  if (trimmed) origins.add(trimmed);
@@ -14536,4 +14733,4 @@ app.get("/blog/:slug/", (c) => {
14536
14733
  export {
14537
14734
  app
14538
14735
  };
14539
- //# sourceMappingURL=server-7QP6HQJJ.js.map
14736
+ //# sourceMappingURL=server-KSEQLZNP.js.map