mcp-scraper 0.4.13 → 0.4.14

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.
@@ -7,7 +7,7 @@ import {
7
7
  registerMemoryMcpTools,
8
8
  registerPaaExtractorMcpTools,
9
9
  registerSerpIntelligenceCaptureTools
10
- } from "../chunk-EA2AANFI.js";
10
+ } from "../chunk-XZ2GPRCM.js";
11
11
  import "../chunk-GWRIO6JT.js";
12
12
  import "../chunk-HPV4VOQX.js";
13
13
  import {
@@ -16,7 +16,7 @@ import {
16
16
  import "../chunk-XGIPATLV.js";
17
17
  import {
18
18
  PACKAGE_VERSION
19
- } from "../chunk-O2D5HGKF.js";
19
+ } from "../chunk-OL65BO4S.js";
20
20
  import "../chunk-M2S27J6Z.js";
21
21
 
22
22
  // bin/mcp-stdio-server.ts
@@ -0,0 +1,7 @@
1
+ // src/version.ts
2
+ var PACKAGE_VERSION = "0.4.14";
3
+
4
+ export {
5
+ PACKAGE_VERSION
6
+ };
7
+ //# sourceMappingURL=chunk-OL65BO4S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["export const PACKAGE_VERSION = '0.4.14'\n"],"mappings":";AAAO,IAAM,kBAAkB;","names":[]}
@@ -17,7 +17,7 @@ import {
17
17
  } from "./chunk-XGIPATLV.js";
18
18
  import {
19
19
  PACKAGE_VERSION
20
- } from "./chunk-O2D5HGKF.js";
20
+ } from "./chunk-OL65BO4S.js";
21
21
  import {
22
22
  sanitizeVendorName
23
23
  } from "./chunk-M2S27J6Z.js";
@@ -8453,4 +8453,4 @@ export {
8453
8453
  registerMemoryMcpTools,
8454
8454
  MemoryMcpToolExecutor
8455
8455
  };
8456
- //# sourceMappingURL=chunk-EA2AANFI.js.map
8456
+ //# sourceMappingURL=chunk-XZ2GPRCM.js.map
@@ -33,7 +33,7 @@ import {
33
33
  registerSerpIntelligenceCaptureTools,
34
34
  sanitizeAttempts,
35
35
  sanitizeHarvestResult
36
- } from "./chunk-EA2AANFI.js";
36
+ } from "./chunk-XZ2GPRCM.js";
37
37
  import {
38
38
  auditImages,
39
39
  getBlobStore
@@ -67,7 +67,7 @@ import {
67
67
  RawMapsOverviewSchema,
68
68
  RawMapsReviewStatsSchema
69
69
  } from "./chunk-XGIPATLV.js";
70
- import "./chunk-O2D5HGKF.js";
70
+ import "./chunk-OL65BO4S.js";
71
71
  import {
72
72
  completeExtractJob,
73
73
  countSuccessfulPages,
@@ -8788,7 +8788,6 @@ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript"
8788
8788
  }
8789
8789
 
8790
8790
  // src/youtube/CaptionFetcher.ts
8791
- import { fal as fal2 } from "@fal-ai/client";
8792
8791
  async function fetchViaYoutubeTranscript(videoId) {
8793
8792
  try {
8794
8793
  const { YoutubeTranscript } = await import("youtube-transcript");
@@ -8861,13 +8860,12 @@ var INNERTUBE_CLIENTS = [
8861
8860
  { name: "IOS", version: "20.10.4", userAgent: "com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)" },
8862
8861
  { name: "ANDROID_VR", version: "1.62.27", userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.27 (Linux; U; Android 12L) gzip" }
8863
8862
  ];
8864
- function isBotBlockStatus(status) {
8865
- return status === "LOGIN_REQUIRED" || status === "AGE_VERIFICATION_REQUIRED";
8866
- }
8867
8863
  async function probePlayer(page, videoId) {
8868
8864
  return page.evaluate(
8869
8865
  async ({ vid, clients }) => {
8870
8866
  let lastStatus = null;
8867
+ let sawBotBlock = false;
8868
+ let sawAccessible = false;
8871
8869
  for (const client2 of clients) {
8872
8870
  const resp = await fetch("/youtubei/v1/player?prettyPrint=false", {
8873
8871
  method: "POST",
@@ -8883,98 +8881,24 @@ async function probePlayer(page, videoId) {
8883
8881
  }
8884
8882
  const data = await resp.json();
8885
8883
  lastStatus = data?.playabilityStatus?.status ?? null;
8886
- const audioFormats = (data?.streamingData?.adaptiveFormats ?? []).filter((f) => typeof f.url === "string" && f.url && typeof f.mimeType === "string" && f.mimeType.startsWith("audio/")).sort((a, b) => (Number(a.contentLength) || Infinity) - (Number(b.contentLength) || Infinity));
8887
- const pick = audioFormats[0] ?? null;
8888
- let captions = null;
8884
+ if (lastStatus === "LOGIN_REQUIRED" || lastStatus === "AGE_VERIFICATION_REQUIRED") sawBotBlock = true;
8885
+ else if (lastStatus === "OK") sawAccessible = true;
8889
8886
  const tracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
8890
8887
  if (tracks.length > 0) {
8891
8888
  const track = tracks.find((t) => t.languageCode === "en") ?? tracks[0];
8892
8889
  const sep = track.baseUrl.includes("?") ? "&" : "?";
8893
8890
  const xmlResp = await fetch(`${track.baseUrl}${sep}fmt=json3`).catch(() => null);
8894
- if (xmlResp?.ok) captions = await xmlResp.text();
8891
+ if (xmlResp?.ok) {
8892
+ const captions = await xmlResp.text();
8893
+ if (captions) return { captions, status: lastStatus, escalate: false };
8894
+ }
8895
8895
  }
8896
- if (!captions && !pick) continue;
8897
- return {
8898
- captions,
8899
- status: lastStatus,
8900
- audio: pick ? {
8901
- url: pick.url,
8902
- mimeType: pick.mimeType,
8903
- bitrate: pick.bitrate ?? null,
8904
- contentLength: pick.contentLength ? Number(pick.contentLength) || null : null,
8905
- approxDurationMs: pick.approxDurationMs ? Number(pick.approxDurationMs) || null : null
8906
- } : null
8907
- };
8908
8896
  }
8909
- return { captions: null, audio: null, status: lastStatus };
8897
+ return { captions: null, status: lastStatus, escalate: sawBotBlock && !sawAccessible };
8910
8898
  },
8911
8899
  { vid: videoId, clients: INNERTUBE_CLIENTS }
8912
8900
  ).catch(() => null);
8913
8901
  }
8914
- var MAX_AUDIO_BYTES = 96 * 1024 * 1024;
8915
- async function downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, audio) {
8916
- if (audio.contentLength && audio.contentLength > MAX_AUDIO_BYTES) {
8917
- console.warn(`[CaptionFetcher] audio stream ${audio.contentLength} bytes exceeds ${MAX_AUDIO_BYTES} byte cap`);
8918
- return null;
8919
- }
8920
- const sep = audio.url.includes("?") ? "&" : "?";
8921
- const url = audio.contentLength ? `${audio.url}${sep}range=0-${audio.contentLength - 1}` : audio.url;
8922
- const result = await page.evaluate(async (u) => {
8923
- try {
8924
- const resp = await fetch(u);
8925
- if (!resp.ok) return { ok: false, error: `http_${resp.status}` };
8926
- const blob = await resp.blob();
8927
- const dataUrl = await new Promise((resolve, reject) => {
8928
- const reader = new FileReader();
8929
- reader.onload = () => resolve(reader.result);
8930
- reader.onerror = () => reject(reader.error);
8931
- reader.readAsDataURL(blob);
8932
- });
8933
- return { ok: true, base64: dataUrl.slice(dataUrl.indexOf(",") + 1) };
8934
- } catch (e) {
8935
- return { ok: false, error: String(e) };
8936
- }
8937
- }, url).catch((e) => ({ ok: false, error: String(e) }));
8938
- if (!result.ok || !result.base64) {
8939
- console.warn(`[CaptionFetcher] audio fetch failed (${result.error ?? "unknown"}) for a ${audio.contentLength ?? 0} byte stream`);
8940
- return null;
8941
- }
8942
- const bytes = Buffer.from(result.base64, "base64");
8943
- if (audio.contentLength && bytes.length < audio.contentLength) {
8944
- console.warn(`[CaptionFetcher] partial audio: ${bytes.length} of ${audio.contentLength} bytes`);
8945
- return null;
8946
- }
8947
- return { bytes, mimeType: audio.mimeType.split(";")[0].trim() };
8948
- }
8949
- async function transcribeWithWizper(videoId, audio, startMs) {
8950
- const falKey = process.env.FAL_KEY;
8951
- if (!falKey) return null;
8952
- try {
8953
- fal2.config({ credentials: falKey });
8954
- const ext = audio.mimeType.includes("webm") ? "webm" : audio.mimeType.includes("mp4") ? "m4a" : "audio";
8955
- const audioFile = new File([new Uint8Array(audio.bytes)], `audio.${ext}`, { type: audio.mimeType });
8956
- const uploadedUrl = await fal2.storage.upload(audioFile);
8957
- const subscribeStartMs = Date.now();
8958
- const result = await fal2.subscribe("fal-ai/wizper", {
8959
- input: { audio_url: uploadedUrl, task: "transcribe", language: "en", chunk_level: "segment" },
8960
- logs: false,
8961
- pollInterval: 3e3
8962
- });
8963
- const subscribeDurationMs = Date.now() - subscribeStartMs;
8964
- const data = result.data;
8965
- const chunks = (data.chunks ?? []).map((c) => ({
8966
- timestamp: c.timestamp,
8967
- text: c.text.trim()
8968
- }));
8969
- void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: subscribeDurationMs / 1e3, unitType: "compute_sec" });
8970
- const text = data.text ?? chunks.map((c) => c.text).join(" ");
8971
- if (!text) return null;
8972
- return { videoId, text, chunks, durationMs: Date.now() - startMs, method: "browser-whisper" };
8973
- } catch (err) {
8974
- console.warn(`[CaptionFetcher] wizper transcription failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
8975
- return null;
8976
- }
8977
- }
8978
8902
  var YT_RESIDENTIAL_LOCATION = process.env.YOUTUBE_PROXY_LOCATION ?? "Austin, Texas";
8979
8903
  async function resolveResidentialProxy(kernelApiKey) {
8980
8904
  try {
@@ -8991,14 +8915,13 @@ async function resolveResidentialProxy(kernelApiKey) {
8991
8915
  return {};
8992
8916
  }
8993
8917
  }
8994
- async function probeAndExtract(videoId, opts) {
8918
+ async function probeCaptions(videoId, kernelProxyIdOverride) {
8995
8919
  const kernelApiKey = browserServiceApiKey();
8996
8920
  if (!kernelApiKey) return null;
8997
- const kernelProxyId = opts?.kernelProxyId ?? browserServiceProxyId();
8921
+ const kernelProxyId = kernelProxyIdOverride ?? browserServiceProxyId();
8998
8922
  const driver = new BrowserDriver();
8999
8923
  return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "browser-innertube" }, async () => {
9000
8924
  let entries = [];
9001
- let audio = null;
9002
8925
  let retryWithFreshIp = false;
9003
8926
  try {
9004
8927
  await driver.launch({ kernelApiKey, kernelProxyId, headless: true, headlessMode: "headless", viewport: { width: 1280, height: 800 }, locale: "en-US" });
@@ -9010,69 +8933,51 @@ async function probeAndExtract(videoId, opts) {
9010
8933
  );
9011
8934
  const probe = await probePlayer(page, videoId);
9012
8935
  if (!probe) return null;
9013
- if (probe.captions && !opts?.skipCaptions) {
8936
+ if (probe.captions) {
9014
8937
  entries = parseTimedtextJson3(probe.captions);
9015
8938
  if (entries.length === 0) entries = parseTimedtextXml(probe.captions);
9016
8939
  }
9017
8940
  if (entries.length === 0) {
9018
- if (!probe.audio) {
9019
- retryWithFreshIp = isBotBlockStatus(probe.status);
9020
- console.warn(`[CaptionFetcher] no captions and no audio stream for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)})`);
9021
- return { entries, audio: null, retryWithFreshIp };
9022
- }
9023
- audio = await downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, probe.audio);
9024
- if (!audio) return { entries, audio: null, retryWithFreshIp: true };
8941
+ retryWithFreshIp = probe.escalate;
8942
+ console.warn(`[CaptionFetcher] no caption track for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)}, escalate=${probe.escalate})`);
9025
8943
  }
9026
8944
  } catch (err) {
9027
8945
  console.warn(`[CaptionFetcher] kernel session failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
9028
8946
  return null;
9029
8947
  } finally {
9030
- const servedSubOp = audio ? "browser-whisper" : "browser-innertube";
9031
- await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: servedSubOp }, () => driver.close());
8948
+ await driver.close();
9032
8949
  }
9033
- return { entries, audio, retryWithFreshIp };
8950
+ return { entries, retryWithFreshIp };
9034
8951
  });
9035
8952
  }
9036
- async function extractWithProxyEscalation(videoId, opts) {
9037
- const direct = await probeAndExtract(videoId, opts);
9038
- if (direct && (direct.entries.length > 0 || direct.audio)) return direct;
9039
- const shouldEscalate = direct === null || direct.retryWithFreshIp;
9040
- if (!shouldEscalate) return direct;
9041
- const kernelApiKey = browserServiceApiKey();
9042
- if (!kernelApiKey) return direct;
9043
- const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
9044
- if (!kernelProxyId) return direct;
9045
- try {
9046
- console.warn(`[CaptionFetcher] ${videoId}: retrying through residential proxy after bot block`);
9047
- const viaProxy = await probeAndExtract(videoId, { ...opts, kernelProxyId });
9048
- return viaProxy ?? direct;
9049
- } finally {
9050
- if (disposableProxyId) {
9051
- await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
9052
- (err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
9053
- );
9054
- }
9055
- }
9056
- }
9057
8953
  async function fetchViaKernelSession(videoId) {
9058
8954
  const start = Date.now();
9059
- const outcome = await extractWithProxyEscalation(videoId);
9060
- if (!outcome) return null;
9061
- if (outcome.entries.length > 0) {
9062
- const chunks = outcome.entries.map((e) => ({
9063
- timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
9064
- text: e.text
9065
- }));
9066
- const text = chunks.map((c) => c.text).join(" ");
9067
- return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
9068
- }
9069
- if (outcome.audio) {
9070
- return runWithCostContext(
9071
- { ...currentCostContext(), op: "yt_transcription", subOp: "browser-whisper" },
9072
- () => transcribeWithWizper(videoId, outcome.audio, start)
9073
- );
8955
+ let outcome = await probeCaptions(videoId);
8956
+ if (!outcome || outcome.entries.length === 0 && outcome.retryWithFreshIp) {
8957
+ const kernelApiKey = browserServiceApiKey();
8958
+ if (kernelApiKey) {
8959
+ const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
8960
+ if (kernelProxyId) {
8961
+ try {
8962
+ console.warn(`[CaptionFetcher] ${videoId}: retrying captions through residential proxy after bot block`);
8963
+ outcome = await probeCaptions(videoId, kernelProxyId) ?? outcome;
8964
+ } finally {
8965
+ if (disposableProxyId) {
8966
+ await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
8967
+ (err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
8968
+ );
8969
+ }
8970
+ }
8971
+ }
8972
+ }
9074
8973
  }
9075
- return null;
8974
+ if (!outcome || outcome.entries.length === 0) return null;
8975
+ const chunks = outcome.entries.map((e) => ({
8976
+ timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
8977
+ text: e.text
8978
+ }));
8979
+ const text = chunks.map((c) => c.text).join(" ");
8980
+ return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
9076
8981
  }
9077
8982
  async function fetchViaResolvedAudio(videoId) {
9078
8983
  if (!mediaResolverConfigured()) return null;
@@ -10485,7 +10390,7 @@ async function extractFacebookOrganicVideoFromPage(page, sourceUrl, quality = "b
10485
10390
  }
10486
10391
 
10487
10392
  // src/api/facebook-ad-routes.ts
10488
- import { fal as fal3 } from "@fal-ai/client";
10393
+ import { fal as fal2 } from "@fal-ai/client";
10489
10394
  var FacebookAdBodySchema = z13.object({
10490
10395
  url: z13.string().trim().optional(),
10491
10396
  libraryId: z13.string().trim().optional(),
@@ -10665,7 +10570,7 @@ facebookAdApp.post("/transcribe", createApiKeyAuth(), async (c) => {
10665
10570
  metadata: { videoUrl }
10666
10571
  });
10667
10572
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
10668
- fal3.config({ credentials: process.env.FAL_KEY });
10573
+ fal2.config({ credentials: process.env.FAL_KEY });
10669
10574
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
10670
10575
  let debited = false;
10671
10576
  try {
@@ -10709,7 +10614,7 @@ facebookAdApp.post("/video-transcribe", createApiKeyAuth(), async (c) => {
10709
10614
  metadata: { url: sourceUrl.href }
10710
10615
  });
10711
10616
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
10712
- fal3.config({ credentials: process.env.FAL_KEY });
10617
+ fal2.config({ credentials: process.env.FAL_KEY });
10713
10618
  const driver = new BrowserDriver();
10714
10619
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
10715
10620
  let debited = false;
@@ -11062,7 +10967,7 @@ var GoogleAdsExtractor = class {
11062
10967
  };
11063
10968
 
11064
10969
  // src/api/google-ads-routes.ts
11065
- import { fal as fal4 } from "@fal-ai/client";
10970
+ import { fal as fal3 } from "@fal-ai/client";
11066
10971
  var GoogleAdsSearchBodySchema = z14.object({
11067
10972
  query: z14.string().trim().min(1, "query is required"),
11068
10973
  region: z14.string().trim().toUpperCase().length(2).optional(),
@@ -11192,7 +11097,7 @@ googleAdsApp.post("/transcribe", createApiKeyAuth(), async (c) => {
11192
11097
  const user = c.get("user");
11193
11098
  const gate = await acquireConcurrencyGate(user, "google_ads_transcribe", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { videoUrl } });
11194
11099
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
11195
- fal4.config({ credentials: process.env.FAL_KEY });
11100
+ fal3.config({ credentials: process.env.FAL_KEY });
11196
11101
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
11197
11102
  let debited = false;
11198
11103
  try {
@@ -22114,4 +22019,4 @@ app.get("/blog/:slug/", (c) => {
22114
22019
  export {
22115
22020
  app
22116
22021
  };
22117
- //# sourceMappingURL=server-HNZG36IP.js.map
22022
+ //# sourceMappingURL=server-TGORO4AJ.js.map