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.
@@ -13705,13 +13705,12 @@ function parseTimedtextXml(xml) {
13705
13705
  }
13706
13706
  return results;
13707
13707
  }
13708
- function isBotBlockStatus(status) {
13709
- return status === "LOGIN_REQUIRED" || status === "AGE_VERIFICATION_REQUIRED";
13710
- }
13711
13708
  async function probePlayer(page, videoId) {
13712
13709
  return page.evaluate(
13713
13710
  async ({ vid, clients }) => {
13714
13711
  let lastStatus = null;
13712
+ let sawBotBlock = false;
13713
+ let sawAccessible = false;
13715
13714
  for (const client2 of clients) {
13716
13715
  const resp = await fetch("/youtubei/v1/player?prettyPrint=false", {
13717
13716
  method: "POST",
@@ -13727,97 +13726,24 @@ async function probePlayer(page, videoId) {
13727
13726
  }
13728
13727
  const data = await resp.json();
13729
13728
  lastStatus = data?.playabilityStatus?.status ?? null;
13730
- 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));
13731
- const pick = audioFormats[0] ?? null;
13732
- let captions = null;
13729
+ if (lastStatus === "LOGIN_REQUIRED" || lastStatus === "AGE_VERIFICATION_REQUIRED") sawBotBlock = true;
13730
+ else if (lastStatus === "OK") sawAccessible = true;
13733
13731
  const tracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
13734
13732
  if (tracks.length > 0) {
13735
13733
  const track = tracks.find((t) => t.languageCode === "en") ?? tracks[0];
13736
13734
  const sep = track.baseUrl.includes("?") ? "&" : "?";
13737
13735
  const xmlResp = await fetch(`${track.baseUrl}${sep}fmt=json3`).catch(() => null);
13738
- if (xmlResp?.ok) captions = await xmlResp.text();
13736
+ if (xmlResp?.ok) {
13737
+ const captions = await xmlResp.text();
13738
+ if (captions) return { captions, status: lastStatus, escalate: false };
13739
+ }
13739
13740
  }
13740
- if (!captions && !pick) continue;
13741
- return {
13742
- captions,
13743
- status: lastStatus,
13744
- audio: pick ? {
13745
- url: pick.url,
13746
- mimeType: pick.mimeType,
13747
- bitrate: pick.bitrate ?? null,
13748
- contentLength: pick.contentLength ? Number(pick.contentLength) || null : null,
13749
- approxDurationMs: pick.approxDurationMs ? Number(pick.approxDurationMs) || null : null
13750
- } : null
13751
- };
13752
13741
  }
13753
- return { captions: null, audio: null, status: lastStatus };
13742
+ return { captions: null, status: lastStatus, escalate: sawBotBlock && !sawAccessible };
13754
13743
  },
13755
13744
  { vid: videoId, clients: INNERTUBE_CLIENTS }
13756
13745
  ).catch(() => null);
13757
13746
  }
13758
- async function downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, audio) {
13759
- if (audio.contentLength && audio.contentLength > MAX_AUDIO_BYTES) {
13760
- console.warn(`[CaptionFetcher] audio stream ${audio.contentLength} bytes exceeds ${MAX_AUDIO_BYTES} byte cap`);
13761
- return null;
13762
- }
13763
- const sep = audio.url.includes("?") ? "&" : "?";
13764
- const url = audio.contentLength ? `${audio.url}${sep}range=0-${audio.contentLength - 1}` : audio.url;
13765
- const result = await page.evaluate(async (u) => {
13766
- try {
13767
- const resp = await fetch(u);
13768
- if (!resp.ok) return { ok: false, error: `http_${resp.status}` };
13769
- const blob = await resp.blob();
13770
- const dataUrl = await new Promise((resolve, reject) => {
13771
- const reader = new FileReader();
13772
- reader.onload = () => resolve(reader.result);
13773
- reader.onerror = () => reject(reader.error);
13774
- reader.readAsDataURL(blob);
13775
- });
13776
- return { ok: true, base64: dataUrl.slice(dataUrl.indexOf(",") + 1) };
13777
- } catch (e) {
13778
- return { ok: false, error: String(e) };
13779
- }
13780
- }, url).catch((e) => ({ ok: false, error: String(e) }));
13781
- if (!result.ok || !result.base64) {
13782
- console.warn(`[CaptionFetcher] audio fetch failed (${result.error ?? "unknown"}) for a ${audio.contentLength ?? 0} byte stream`);
13783
- return null;
13784
- }
13785
- const bytes = Buffer.from(result.base64, "base64");
13786
- if (audio.contentLength && bytes.length < audio.contentLength) {
13787
- console.warn(`[CaptionFetcher] partial audio: ${bytes.length} of ${audio.contentLength} bytes`);
13788
- return null;
13789
- }
13790
- return { bytes, mimeType: audio.mimeType.split(";")[0].trim() };
13791
- }
13792
- async function transcribeWithWizper(videoId, audio, startMs) {
13793
- const falKey = process.env.FAL_KEY;
13794
- if (!falKey) return null;
13795
- try {
13796
- import_client4.fal.config({ credentials: falKey });
13797
- const ext = audio.mimeType.includes("webm") ? "webm" : audio.mimeType.includes("mp4") ? "m4a" : "audio";
13798
- const audioFile = new File([new Uint8Array(audio.bytes)], `audio.${ext}`, { type: audio.mimeType });
13799
- const uploadedUrl = await import_client4.fal.storage.upload(audioFile);
13800
- const subscribeStartMs = Date.now();
13801
- const result = await import_client4.fal.subscribe("fal-ai/wizper", {
13802
- input: { audio_url: uploadedUrl, task: "transcribe", language: "en", chunk_level: "segment" },
13803
- logs: false,
13804
- pollInterval: 3e3
13805
- });
13806
- const subscribeDurationMs = Date.now() - subscribeStartMs;
13807
- const data = result.data;
13808
- const chunks = (data.chunks ?? []).map((c) => ({
13809
- timestamp: c.timestamp,
13810
- text: c.text.trim()
13811
- }));
13812
- void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: subscribeDurationMs / 1e3, unitType: "compute_sec" });
13813
- const text = data.text ?? chunks.map((c) => c.text).join(" ");
13814
- if (!text) return null;
13815
- return { videoId, text, chunks, durationMs: Date.now() - startMs, method: "browser-whisper" };
13816
- } catch (err) {
13817
- console.warn(`[CaptionFetcher] wizper transcription failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
13818
- return null;
13819
- }
13820
- }
13821
13747
  async function resolveResidentialProxy(kernelApiKey) {
13822
13748
  try {
13823
13749
  const resolved = await resolveKernelProxyId({
@@ -13833,14 +13759,13 @@ async function resolveResidentialProxy(kernelApiKey) {
13833
13759
  return {};
13834
13760
  }
13835
13761
  }
13836
- async function probeAndExtract(videoId, opts) {
13762
+ async function probeCaptions(videoId, kernelProxyIdOverride) {
13837
13763
  const kernelApiKey = browserServiceApiKey();
13838
13764
  if (!kernelApiKey) return null;
13839
- const kernelProxyId = opts?.kernelProxyId ?? browserServiceProxyId();
13765
+ const kernelProxyId = kernelProxyIdOverride ?? browserServiceProxyId();
13840
13766
  const driver = new BrowserDriver();
13841
13767
  return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "browser-innertube" }, async () => {
13842
13768
  let entries = [];
13843
- let audio = null;
13844
13769
  let retryWithFreshIp = false;
13845
13770
  try {
13846
13771
  await driver.launch({ kernelApiKey, kernelProxyId, headless: true, headlessMode: "headless", viewport: { width: 1280, height: 800 }, locale: "en-US" });
@@ -13852,69 +13777,51 @@ async function probeAndExtract(videoId, opts) {
13852
13777
  );
13853
13778
  const probe = await probePlayer(page, videoId);
13854
13779
  if (!probe) return null;
13855
- if (probe.captions && !opts?.skipCaptions) {
13780
+ if (probe.captions) {
13856
13781
  entries = parseTimedtextJson3(probe.captions);
13857
13782
  if (entries.length === 0) entries = parseTimedtextXml(probe.captions);
13858
13783
  }
13859
13784
  if (entries.length === 0) {
13860
- if (!probe.audio) {
13861
- retryWithFreshIp = isBotBlockStatus(probe.status);
13862
- console.warn(`[CaptionFetcher] no captions and no audio stream for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)})`);
13863
- return { entries, audio: null, retryWithFreshIp };
13864
- }
13865
- audio = await downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, probe.audio);
13866
- if (!audio) return { entries, audio: null, retryWithFreshIp: true };
13785
+ retryWithFreshIp = probe.escalate;
13786
+ console.warn(`[CaptionFetcher] no caption track for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)}, escalate=${probe.escalate})`);
13867
13787
  }
13868
13788
  } catch (err) {
13869
13789
  console.warn(`[CaptionFetcher] kernel session failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
13870
13790
  return null;
13871
13791
  } finally {
13872
- const servedSubOp = audio ? "browser-whisper" : "browser-innertube";
13873
- await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: servedSubOp }, () => driver.close());
13792
+ await driver.close();
13874
13793
  }
13875
- return { entries, audio, retryWithFreshIp };
13794
+ return { entries, retryWithFreshIp };
13876
13795
  });
13877
13796
  }
13878
- async function extractWithProxyEscalation(videoId, opts) {
13879
- const direct = await probeAndExtract(videoId, opts);
13880
- if (direct && (direct.entries.length > 0 || direct.audio)) return direct;
13881
- const shouldEscalate = direct === null || direct.retryWithFreshIp;
13882
- if (!shouldEscalate) return direct;
13883
- const kernelApiKey = browserServiceApiKey();
13884
- if (!kernelApiKey) return direct;
13885
- const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
13886
- if (!kernelProxyId) return direct;
13887
- try {
13888
- console.warn(`[CaptionFetcher] ${videoId}: retrying through residential proxy after bot block`);
13889
- const viaProxy = await probeAndExtract(videoId, { ...opts, kernelProxyId });
13890
- return viaProxy ?? direct;
13891
- } finally {
13892
- if (disposableProxyId) {
13893
- await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
13894
- (err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
13895
- );
13896
- }
13897
- }
13898
- }
13899
13797
  async function fetchViaKernelSession(videoId) {
13900
13798
  const start = Date.now();
13901
- const outcome = await extractWithProxyEscalation(videoId);
13902
- if (!outcome) return null;
13903
- if (outcome.entries.length > 0) {
13904
- const chunks = outcome.entries.map((e) => ({
13905
- timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
13906
- text: e.text
13907
- }));
13908
- const text = chunks.map((c) => c.text).join(" ");
13909
- return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
13910
- }
13911
- if (outcome.audio) {
13912
- return runWithCostContext(
13913
- { ...currentCostContext(), op: "yt_transcription", subOp: "browser-whisper" },
13914
- () => transcribeWithWizper(videoId, outcome.audio, start)
13915
- );
13799
+ let outcome = await probeCaptions(videoId);
13800
+ if (!outcome || outcome.entries.length === 0 && outcome.retryWithFreshIp) {
13801
+ const kernelApiKey = browserServiceApiKey();
13802
+ if (kernelApiKey) {
13803
+ const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
13804
+ if (kernelProxyId) {
13805
+ try {
13806
+ console.warn(`[CaptionFetcher] ${videoId}: retrying captions through residential proxy after bot block`);
13807
+ outcome = await probeCaptions(videoId, kernelProxyId) ?? outcome;
13808
+ } finally {
13809
+ if (disposableProxyId) {
13810
+ await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
13811
+ (err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
13812
+ );
13813
+ }
13814
+ }
13815
+ }
13816
+ }
13916
13817
  }
13917
- return null;
13818
+ if (!outcome || outcome.entries.length === 0) return null;
13819
+ const chunks = outcome.entries.map((e) => ({
13820
+ timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
13821
+ text: e.text
13822
+ }));
13823
+ const text = chunks.map((c) => c.text).join(" ");
13824
+ return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
13918
13825
  }
13919
13826
  async function fetchViaResolvedAudio(videoId) {
13920
13827
  if (!mediaResolverConfigured()) return null;
@@ -13964,7 +13871,7 @@ async function fetchCaptions(videoId) {
13964
13871
  `Could not transcribe ${videoId}: it has no caption track and its audio could not be retrieved.`
13965
13872
  );
13966
13873
  }
13967
- var import_client4, INNERTUBE_CLIENTS, MAX_AUDIO_BYTES, YT_RESIDENTIAL_LOCATION;
13874
+ var INNERTUBE_CLIENTS, YT_RESIDENTIAL_LOCATION;
13968
13875
  var init_CaptionFetcher = __esm({
13969
13876
  "src/youtube/CaptionFetcher.ts"() {
13970
13877
  "use strict";
@@ -13973,7 +13880,6 @@ var init_CaptionFetcher = __esm({
13973
13880
  init_kernel_proxy_resolver();
13974
13881
  init_audio_resolver();
13975
13882
  init_media_transcription();
13976
- import_client4 = require("@fal-ai/client");
13977
13883
  init_cost_telemetry();
13978
13884
  init_cost_context();
13979
13885
  INNERTUBE_CLIENTS = [
@@ -13981,7 +13887,6 @@ var init_CaptionFetcher = __esm({
13981
13887
  { 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;)" },
13982
13888
  { name: "ANDROID_VR", version: "1.62.27", userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.27 (Linux; U; Android 12L) gzip" }
13983
13889
  ];
13984
- MAX_AUDIO_BYTES = 96 * 1024 * 1024;
13985
13890
  YT_RESIDENTIAL_LOCATION = process.env.YOUTUBE_PROXY_LOCATION ?? "Austin, Texas";
13986
13891
  }
13987
13892
  });
@@ -15560,7 +15465,7 @@ async function kernelLaunchOptsResidential() {
15560
15465
  }
15561
15466
  return { headless: true, kernelApiKey: browserServiceApiKey(), kernelProxyId: proxyId, viewport: { width: 1280, height: 900 }, locale: "en-US" };
15562
15467
  }
15563
- var import_hono5, import_zod16, import_client5, FacebookAdBodySchema, FacebookPageIntelBodySchema, FacebookTranscribeBodySchema, FacebookVideoTranscribeBodySchema, FacebookSearchBodySchema, FacebookMediaBodySchema, facebookAdApp, ALLOWED_MEDIA_HOSTS;
15468
+ var import_hono5, import_zod16, import_client4, FacebookAdBodySchema, FacebookPageIntelBodySchema, FacebookTranscribeBodySchema, FacebookVideoTranscribeBodySchema, FacebookSearchBodySchema, FacebookMediaBodySchema, facebookAdApp, ALLOWED_MEDIA_HOSTS;
15564
15469
  var init_facebook_ad_routes = __esm({
15565
15470
  "src/api/facebook-ad-routes.ts"() {
15566
15471
  "use strict";
@@ -15576,7 +15481,7 @@ var init_facebook_ad_routes = __esm({
15576
15481
  init_FacebookOrganicVideoExtractor();
15577
15482
  init_kernel_proxy_resolver();
15578
15483
  init_schemas3();
15579
- import_client5 = require("@fal-ai/client");
15484
+ import_client4 = require("@fal-ai/client");
15580
15485
  init_api_auth();
15581
15486
  init_url_utils();
15582
15487
  init_concurrency_gates();
@@ -15719,7 +15624,7 @@ var init_facebook_ad_routes = __esm({
15719
15624
  metadata: { videoUrl }
15720
15625
  });
15721
15626
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
15722
- import_client5.fal.config({ credentials: process.env.FAL_KEY });
15627
+ import_client4.fal.config({ credentials: process.env.FAL_KEY });
15723
15628
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
15724
15629
  let debited = false;
15725
15630
  try {
@@ -15763,7 +15668,7 @@ var init_facebook_ad_routes = __esm({
15763
15668
  metadata: { url: sourceUrl.href }
15764
15669
  });
15765
15670
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
15766
- import_client5.fal.config({ credentials: process.env.FAL_KEY });
15671
+ import_client4.fal.config({ credentials: process.env.FAL_KEY });
15767
15672
  const driver = new BrowserDriver();
15768
15673
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
15769
15674
  let debited = false;
@@ -16144,7 +16049,7 @@ async function kernelLaunchOptsResidential2() {
16144
16049
  function isBlockedMessage(msg) {
16145
16050
  return msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha");
16146
16051
  }
16147
- var import_hono6, import_zod17, import_client6, GoogleAdsSearchBodySchema, GoogleAdsPageIntelBodySchema, GoogleAdsTranscribeBodySchema, googleAdsApp;
16052
+ var import_hono6, import_zod17, import_client5, GoogleAdsSearchBodySchema, GoogleAdsPageIntelBodySchema, GoogleAdsTranscribeBodySchema, googleAdsApp;
16148
16053
  var init_google_ads_routes = __esm({
16149
16054
  "src/api/google-ads-routes.ts"() {
16150
16055
  "use strict";
@@ -16157,7 +16062,7 @@ var init_google_ads_routes = __esm({
16157
16062
  init_GoogleAdsExtractor();
16158
16063
  init_kernel_proxy_resolver();
16159
16064
  init_schemas3();
16160
- import_client6 = require("@fal-ai/client");
16065
+ import_client5 = require("@fal-ai/client");
16161
16066
  init_api_auth();
16162
16067
  init_url_utils();
16163
16068
  init_concurrency_gates();
@@ -16268,7 +16173,7 @@ var init_google_ads_routes = __esm({
16268
16173
  const user = c.get("user");
16269
16174
  const gate = await acquireConcurrencyGate(user, "google_ads_transcribe", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { videoUrl } });
16270
16175
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
16271
- import_client6.fal.config({ credentials: process.env.FAL_KEY });
16176
+ import_client5.fal.config({ credentials: process.env.FAL_KEY });
16272
16177
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
16273
16178
  let debited = false;
16274
16179
  try {
@@ -27399,7 +27304,7 @@ var PACKAGE_VERSION;
27399
27304
  var init_version = __esm({
27400
27305
  "src/version.ts"() {
27401
27306
  "use strict";
27402
- PACKAGE_VERSION = "0.4.13";
27307
+ PACKAGE_VERSION = "0.4.14";
27403
27308
  }
27404
27309
  });
27405
27310