mcp-scraper 0.4.12 → 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.
- package/dist/bin/api-server.cjs +240 -218
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +1 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/{chunk-2YY46QYT.js → chunk-AUCXKRRH.js} +3 -1
- package/dist/chunk-OL65BO4S.js +7 -0
- package/dist/chunk-OL65BO4S.js.map +1 -0
- package/dist/{chunk-EMY7ELRU.js → chunk-SIPXEFTR.js} +14 -10
- package/dist/chunk-SIPXEFTR.js.map +1 -0
- package/dist/{chunk-ONIOF5XW.js → chunk-VQV7MB2S.js} +2 -2
- package/dist/{chunk-6EXP6DQG.js → chunk-XZ2GPRCM.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{server-UNID3SJU.js → server-TGORO4AJ.js} +219 -208
- package/dist/server-TGORO4AJ.js.map +1 -0
- package/dist/{site-extract-repository-HTIY52MW.js → site-extract-repository-6X2C7RLF.js} +3 -3
- package/dist/{worker-HTYZAYGB.js → worker-WCSKUAOT.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-EMY7ELRU.js.map +0 -1
- package/dist/chunk-SHXJQQOH.js +0 -7
- package/dist/chunk-SHXJQQOH.js.map +0 -1
- package/dist/server-UNID3SJU.js.map +0 -1
- /package/dist/{chunk-2YY46QYT.js.map → chunk-AUCXKRRH.js.map} +0 -0
- /package/dist/{chunk-ONIOF5XW.js.map → chunk-VQV7MB2S.js.map} +0 -0
- /package/dist/{chunk-6EXP6DQG.js.map → chunk-XZ2GPRCM.js.map} +0 -0
- /package/dist/{site-extract-repository-HTIY52MW.js.map → site-extract-repository-6X2C7RLF.js.map} +0 -0
- /package/dist/{worker-HTYZAYGB.js.map → worker-WCSKUAOT.js.map} +0 -0
|
@@ -11,12 +11,14 @@ import {
|
|
|
11
11
|
currentCostContext,
|
|
12
12
|
deleteKernelProxyId,
|
|
13
13
|
harvest,
|
|
14
|
+
kernelCostUsd,
|
|
14
15
|
migrateCostTelemetry,
|
|
15
16
|
recordKernelSession,
|
|
16
17
|
recordVendorUsage,
|
|
17
18
|
resolveKernelProxyId,
|
|
18
|
-
runWithCostContext
|
|
19
|
-
|
|
19
|
+
runWithCostContext,
|
|
20
|
+
vendorCostUsd
|
|
21
|
+
} from "./chunk-AUCXKRRH.js";
|
|
20
22
|
import {
|
|
21
23
|
HttpMcpToolExecutor,
|
|
22
24
|
MemoryMcpToolExecutor,
|
|
@@ -31,7 +33,7 @@ import {
|
|
|
31
33
|
registerSerpIntelligenceCaptureTools,
|
|
32
34
|
sanitizeAttempts,
|
|
33
35
|
sanitizeHarvestResult
|
|
34
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-XZ2GPRCM.js";
|
|
35
37
|
import {
|
|
36
38
|
auditImages,
|
|
37
39
|
getBlobStore
|
|
@@ -65,7 +67,7 @@ import {
|
|
|
65
67
|
RawMapsOverviewSchema,
|
|
66
68
|
RawMapsReviewStatsSchema
|
|
67
69
|
} from "./chunk-XGIPATLV.js";
|
|
68
|
-
import "./chunk-
|
|
70
|
+
import "./chunk-OL65BO4S.js";
|
|
69
71
|
import {
|
|
70
72
|
completeExtractJob,
|
|
71
73
|
countSuccessfulPages,
|
|
@@ -76,7 +78,7 @@ import {
|
|
|
76
78
|
saveExtractPages,
|
|
77
79
|
setExtractJobTotal,
|
|
78
80
|
settleExtractJob
|
|
79
|
-
} from "./chunk-
|
|
81
|
+
} from "./chunk-VQV7MB2S.js";
|
|
80
82
|
import {
|
|
81
83
|
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
82
84
|
CONCURRENCY_PRICE_ID,
|
|
@@ -86,6 +88,8 @@ import {
|
|
|
86
88
|
MC_COSTS,
|
|
87
89
|
MC_PER_CREDIT,
|
|
88
90
|
MC_PER_USD,
|
|
91
|
+
MEDIA_TRANSCRIBE_HOLD_MC,
|
|
92
|
+
MEDIA_TRANSCRIBE_MARGIN_MULTIPLE,
|
|
89
93
|
MEMORY_AI_MARGIN_MULTIPLE,
|
|
90
94
|
MEMORY_PLANS,
|
|
91
95
|
MEMORY_PLAN_QUOTA,
|
|
@@ -97,7 +101,7 @@ import {
|
|
|
97
101
|
browserActiveCostMc,
|
|
98
102
|
concurrencySlotBillingInfo,
|
|
99
103
|
insufficientBalanceResponse
|
|
100
|
-
} from "./chunk-
|
|
104
|
+
} from "./chunk-SIPXEFTR.js";
|
|
101
105
|
import {
|
|
102
106
|
CaptchaError,
|
|
103
107
|
RECAPTCHA_INSTRUCTIONS,
|
|
@@ -8699,8 +8703,91 @@ async function ytHarvest(rawOptions) {
|
|
|
8699
8703
|
);
|
|
8700
8704
|
}
|
|
8701
8705
|
|
|
8702
|
-
// src/youtube/
|
|
8706
|
+
// src/youtube/audio-resolver.ts
|
|
8707
|
+
function mediaResolverUrl() {
|
|
8708
|
+
return process.env.FRAMES_SERVICE_URL?.trim() || void 0;
|
|
8709
|
+
}
|
|
8710
|
+
function mediaResolverSecret() {
|
|
8711
|
+
return process.env.FRAMES_SERVICE_SECRET?.trim() || void 0;
|
|
8712
|
+
}
|
|
8713
|
+
function mediaResolverConfigured() {
|
|
8714
|
+
return Boolean(mediaResolverUrl() && mediaResolverSecret());
|
|
8715
|
+
}
|
|
8716
|
+
var RESOLVE_TIMEOUT_MS = 24e4;
|
|
8717
|
+
async function resolveYouTubeAudio(videoId) {
|
|
8718
|
+
const base = mediaResolverUrl();
|
|
8719
|
+
const secret2 = mediaResolverSecret();
|
|
8720
|
+
if (!base || !secret2) return null;
|
|
8721
|
+
const controller = new AbortController();
|
|
8722
|
+
const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
|
|
8723
|
+
try {
|
|
8724
|
+
const resp = await fetch(`${base.replace(/\/+$/, "")}/resolve-audio`, {
|
|
8725
|
+
method: "POST",
|
|
8726
|
+
headers: { "content-type": "application/json", "x-frames-secret": secret2 },
|
|
8727
|
+
body: JSON.stringify({ videoUrl: `https://www.youtube.com/watch?v=${videoId}` }),
|
|
8728
|
+
signal: controller.signal
|
|
8729
|
+
});
|
|
8730
|
+
if (!resp.ok) {
|
|
8731
|
+
const detail = await resp.text().catch(() => "");
|
|
8732
|
+
console.warn(`[audio-resolver] ${videoId}: http_${resp.status} ${detail.slice(0, 200)}`);
|
|
8733
|
+
return null;
|
|
8734
|
+
}
|
|
8735
|
+
const data = await resp.json();
|
|
8736
|
+
if (!data.mediaUrl) {
|
|
8737
|
+
console.warn(`[audio-resolver] ${videoId}: response missing mediaUrl`);
|
|
8738
|
+
return null;
|
|
8739
|
+
}
|
|
8740
|
+
return {
|
|
8741
|
+
mediaUrl: data.mediaUrl,
|
|
8742
|
+
durationS: Number(data.durationS) || 0,
|
|
8743
|
+
sizeBytes: Number(data.sizeBytes) || 0,
|
|
8744
|
+
mimeType: data.mimeType ?? "audio/mp4"
|
|
8745
|
+
};
|
|
8746
|
+
} catch (err) {
|
|
8747
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8748
|
+
console.warn(`[audio-resolver] ${videoId}: resolve failed \u2014 ${msg}`);
|
|
8749
|
+
return null;
|
|
8750
|
+
} finally {
|
|
8751
|
+
clearTimeout(timer);
|
|
8752
|
+
}
|
|
8753
|
+
}
|
|
8754
|
+
|
|
8755
|
+
// src/services/media-transcription.ts
|
|
8703
8756
|
import { fal } from "@fal-ai/client";
|
|
8757
|
+
function transcriptMarkdown(title, text, chunks, durationMs) {
|
|
8758
|
+
const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
|
|
8759
|
+
const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
|
|
8760
|
+
if (chunks.length) {
|
|
8761
|
+
lines.push("## Timestamped Segments", "");
|
|
8762
|
+
for (const ch of chunks) {
|
|
8763
|
+
lines.push(`**[${fmtTs2(ch.timestamp[0])} -> ${fmtTs2(ch.timestamp[1])}]** ${ch.text.trim()}`, "");
|
|
8764
|
+
}
|
|
8765
|
+
}
|
|
8766
|
+
return lines.join("\n");
|
|
8767
|
+
}
|
|
8768
|
+
async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript") {
|
|
8769
|
+
const startMs = Date.now();
|
|
8770
|
+
const result = await fal.subscribe("fal-ai/wizper", {
|
|
8771
|
+
input: { audio_url: mediaUrl, task: "transcribe", language: "en" },
|
|
8772
|
+
logs: false,
|
|
8773
|
+
pollInterval: 3e3
|
|
8774
|
+
});
|
|
8775
|
+
const data = result.data;
|
|
8776
|
+
const text = data.text ?? "";
|
|
8777
|
+
const chunks = data.chunks ?? [];
|
|
8778
|
+
const durationMs = Date.now() - startMs;
|
|
8779
|
+
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: durationMs / 1e3, unitType: "compute_sec" });
|
|
8780
|
+
const costUsd = vendorCostUsd("fal_wizper", durationMs / 1e3);
|
|
8781
|
+
return {
|
|
8782
|
+
text,
|
|
8783
|
+
chunks,
|
|
8784
|
+
durationMs,
|
|
8785
|
+
costUsd,
|
|
8786
|
+
markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs)
|
|
8787
|
+
};
|
|
8788
|
+
}
|
|
8789
|
+
|
|
8790
|
+
// src/youtube/CaptionFetcher.ts
|
|
8704
8791
|
async function fetchViaYoutubeTranscript(videoId) {
|
|
8705
8792
|
try {
|
|
8706
8793
|
const { YoutubeTranscript } = await import("youtube-transcript");
|
|
@@ -8773,13 +8860,12 @@ var INNERTUBE_CLIENTS = [
|
|
|
8773
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;)" },
|
|
8774
8861
|
{ name: "ANDROID_VR", version: "1.62.27", userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.27 (Linux; U; Android 12L) gzip" }
|
|
8775
8862
|
];
|
|
8776
|
-
function isBotBlockStatus(status) {
|
|
8777
|
-
return status === "LOGIN_REQUIRED" || status === "AGE_VERIFICATION_REQUIRED";
|
|
8778
|
-
}
|
|
8779
8863
|
async function probePlayer(page, videoId) {
|
|
8780
8864
|
return page.evaluate(
|
|
8781
8865
|
async ({ vid, clients }) => {
|
|
8782
8866
|
let lastStatus = null;
|
|
8867
|
+
let sawBotBlock = false;
|
|
8868
|
+
let sawAccessible = false;
|
|
8783
8869
|
for (const client2 of clients) {
|
|
8784
8870
|
const resp = await fetch("/youtubei/v1/player?prettyPrint=false", {
|
|
8785
8871
|
method: "POST",
|
|
@@ -8795,98 +8881,24 @@ async function probePlayer(page, videoId) {
|
|
|
8795
8881
|
}
|
|
8796
8882
|
const data = await resp.json();
|
|
8797
8883
|
lastStatus = data?.playabilityStatus?.status ?? null;
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
let captions = null;
|
|
8884
|
+
if (lastStatus === "LOGIN_REQUIRED" || lastStatus === "AGE_VERIFICATION_REQUIRED") sawBotBlock = true;
|
|
8885
|
+
else if (lastStatus === "OK") sawAccessible = true;
|
|
8801
8886
|
const tracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
|
|
8802
8887
|
if (tracks.length > 0) {
|
|
8803
8888
|
const track = tracks.find((t) => t.languageCode === "en") ?? tracks[0];
|
|
8804
8889
|
const sep = track.baseUrl.includes("?") ? "&" : "?";
|
|
8805
8890
|
const xmlResp = await fetch(`${track.baseUrl}${sep}fmt=json3`).catch(() => null);
|
|
8806
|
-
if (xmlResp?.ok)
|
|
8891
|
+
if (xmlResp?.ok) {
|
|
8892
|
+
const captions = await xmlResp.text();
|
|
8893
|
+
if (captions) return { captions, status: lastStatus, escalate: false };
|
|
8894
|
+
}
|
|
8807
8895
|
}
|
|
8808
|
-
if (!captions && !pick) continue;
|
|
8809
|
-
return {
|
|
8810
|
-
captions,
|
|
8811
|
-
status: lastStatus,
|
|
8812
|
-
audio: pick ? {
|
|
8813
|
-
url: pick.url,
|
|
8814
|
-
mimeType: pick.mimeType,
|
|
8815
|
-
bitrate: pick.bitrate ?? null,
|
|
8816
|
-
contentLength: pick.contentLength ? Number(pick.contentLength) || null : null,
|
|
8817
|
-
approxDurationMs: pick.approxDurationMs ? Number(pick.approxDurationMs) || null : null
|
|
8818
|
-
} : null
|
|
8819
|
-
};
|
|
8820
8896
|
}
|
|
8821
|
-
return { captions: null,
|
|
8897
|
+
return { captions: null, status: lastStatus, escalate: sawBotBlock && !sawAccessible };
|
|
8822
8898
|
},
|
|
8823
8899
|
{ vid: videoId, clients: INNERTUBE_CLIENTS }
|
|
8824
8900
|
).catch(() => null);
|
|
8825
8901
|
}
|
|
8826
|
-
var MAX_AUDIO_BYTES = 96 * 1024 * 1024;
|
|
8827
|
-
async function downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, audio) {
|
|
8828
|
-
if (audio.contentLength && audio.contentLength > MAX_AUDIO_BYTES) {
|
|
8829
|
-
console.warn(`[CaptionFetcher] audio stream ${audio.contentLength} bytes exceeds ${MAX_AUDIO_BYTES} byte cap`);
|
|
8830
|
-
return null;
|
|
8831
|
-
}
|
|
8832
|
-
const sep = audio.url.includes("?") ? "&" : "?";
|
|
8833
|
-
const url = audio.contentLength ? `${audio.url}${sep}range=0-${audio.contentLength - 1}` : audio.url;
|
|
8834
|
-
const result = await page.evaluate(async (u) => {
|
|
8835
|
-
try {
|
|
8836
|
-
const resp = await fetch(u);
|
|
8837
|
-
if (!resp.ok) return { ok: false, error: `http_${resp.status}` };
|
|
8838
|
-
const blob = await resp.blob();
|
|
8839
|
-
const dataUrl = await new Promise((resolve, reject) => {
|
|
8840
|
-
const reader = new FileReader();
|
|
8841
|
-
reader.onload = () => resolve(reader.result);
|
|
8842
|
-
reader.onerror = () => reject(reader.error);
|
|
8843
|
-
reader.readAsDataURL(blob);
|
|
8844
|
-
});
|
|
8845
|
-
return { ok: true, base64: dataUrl.slice(dataUrl.indexOf(",") + 1) };
|
|
8846
|
-
} catch (e) {
|
|
8847
|
-
return { ok: false, error: String(e) };
|
|
8848
|
-
}
|
|
8849
|
-
}, url).catch((e) => ({ ok: false, error: String(e) }));
|
|
8850
|
-
if (!result.ok || !result.base64) {
|
|
8851
|
-
console.warn(`[CaptionFetcher] audio fetch failed (${result.error ?? "unknown"}) for a ${audio.contentLength ?? 0} byte stream`);
|
|
8852
|
-
return null;
|
|
8853
|
-
}
|
|
8854
|
-
const bytes = Buffer.from(result.base64, "base64");
|
|
8855
|
-
if (audio.contentLength && bytes.length < audio.contentLength) {
|
|
8856
|
-
console.warn(`[CaptionFetcher] partial audio: ${bytes.length} of ${audio.contentLength} bytes`);
|
|
8857
|
-
return null;
|
|
8858
|
-
}
|
|
8859
|
-
return { bytes, mimeType: audio.mimeType.split(";")[0].trim() };
|
|
8860
|
-
}
|
|
8861
|
-
async function transcribeWithWizper(videoId, audio, startMs) {
|
|
8862
|
-
const falKey = process.env.FAL_KEY;
|
|
8863
|
-
if (!falKey) return null;
|
|
8864
|
-
try {
|
|
8865
|
-
fal.config({ credentials: falKey });
|
|
8866
|
-
const ext = audio.mimeType.includes("webm") ? "webm" : audio.mimeType.includes("mp4") ? "m4a" : "audio";
|
|
8867
|
-
const audioFile = new File([new Uint8Array(audio.bytes)], `audio.${ext}`, { type: audio.mimeType });
|
|
8868
|
-
const uploadedUrl = await fal.storage.upload(audioFile);
|
|
8869
|
-
const subscribeStartMs = Date.now();
|
|
8870
|
-
const result = await fal.subscribe("fal-ai/wizper", {
|
|
8871
|
-
input: { audio_url: uploadedUrl, task: "transcribe", language: "en", chunk_level: "segment" },
|
|
8872
|
-
logs: false,
|
|
8873
|
-
pollInterval: 3e3
|
|
8874
|
-
});
|
|
8875
|
-
const subscribeDurationMs = Date.now() - subscribeStartMs;
|
|
8876
|
-
const data = result.data;
|
|
8877
|
-
const chunks = (data.chunks ?? []).map((c) => ({
|
|
8878
|
-
timestamp: c.timestamp,
|
|
8879
|
-
text: c.text.trim()
|
|
8880
|
-
}));
|
|
8881
|
-
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: subscribeDurationMs / 1e3, unitType: "compute_sec" });
|
|
8882
|
-
const text = data.text ?? chunks.map((c) => c.text).join(" ");
|
|
8883
|
-
if (!text) return null;
|
|
8884
|
-
return { videoId, text, chunks, durationMs: Date.now() - startMs, method: "browser-whisper" };
|
|
8885
|
-
} catch (err) {
|
|
8886
|
-
console.warn(`[CaptionFetcher] wizper transcription failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8887
|
-
return null;
|
|
8888
|
-
}
|
|
8889
|
-
}
|
|
8890
8902
|
var YT_RESIDENTIAL_LOCATION = process.env.YOUTUBE_PROXY_LOCATION ?? "Austin, Texas";
|
|
8891
8903
|
async function resolveResidentialProxy(kernelApiKey) {
|
|
8892
8904
|
try {
|
|
@@ -8903,14 +8915,13 @@ async function resolveResidentialProxy(kernelApiKey) {
|
|
|
8903
8915
|
return {};
|
|
8904
8916
|
}
|
|
8905
8917
|
}
|
|
8906
|
-
async function
|
|
8918
|
+
async function probeCaptions(videoId, kernelProxyIdOverride) {
|
|
8907
8919
|
const kernelApiKey = browserServiceApiKey();
|
|
8908
8920
|
if (!kernelApiKey) return null;
|
|
8909
|
-
const kernelProxyId =
|
|
8921
|
+
const kernelProxyId = kernelProxyIdOverride ?? browserServiceProxyId();
|
|
8910
8922
|
const driver = new BrowserDriver();
|
|
8911
8923
|
return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "browser-innertube" }, async () => {
|
|
8912
8924
|
let entries = [];
|
|
8913
|
-
let audio = null;
|
|
8914
8925
|
let retryWithFreshIp = false;
|
|
8915
8926
|
try {
|
|
8916
8927
|
await driver.launch({ kernelApiKey, kernelProxyId, headless: true, headlessMode: "headless", viewport: { width: 1280, height: 800 }, locale: "en-US" });
|
|
@@ -8922,69 +8933,73 @@ async function probeAndExtract(videoId, opts) {
|
|
|
8922
8933
|
);
|
|
8923
8934
|
const probe = await probePlayer(page, videoId);
|
|
8924
8935
|
if (!probe) return null;
|
|
8925
|
-
if (probe.captions
|
|
8936
|
+
if (probe.captions) {
|
|
8926
8937
|
entries = parseTimedtextJson3(probe.captions);
|
|
8927
8938
|
if (entries.length === 0) entries = parseTimedtextXml(probe.captions);
|
|
8928
8939
|
}
|
|
8929
8940
|
if (entries.length === 0) {
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
console.warn(`[CaptionFetcher] no captions and no audio stream for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)})`);
|
|
8933
|
-
return { entries, audio: null, retryWithFreshIp };
|
|
8934
|
-
}
|
|
8935
|
-
audio = await downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, probe.audio);
|
|
8936
|
-
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})`);
|
|
8937
8943
|
}
|
|
8938
8944
|
} catch (err) {
|
|
8939
8945
|
console.warn(`[CaptionFetcher] kernel session failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8940
8946
|
return null;
|
|
8941
8947
|
} finally {
|
|
8942
|
-
|
|
8943
|
-
await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: servedSubOp }, () => driver.close());
|
|
8948
|
+
await driver.close();
|
|
8944
8949
|
}
|
|
8945
|
-
return { entries,
|
|
8950
|
+
return { entries, retryWithFreshIp };
|
|
8946
8951
|
});
|
|
8947
8952
|
}
|
|
8948
|
-
async function extractWithProxyEscalation(videoId, opts) {
|
|
8949
|
-
const direct = await probeAndExtract(videoId, opts);
|
|
8950
|
-
if (direct && (direct.entries.length > 0 || direct.audio)) return direct;
|
|
8951
|
-
const shouldEscalate = direct === null || direct.retryWithFreshIp;
|
|
8952
|
-
if (!shouldEscalate) return direct;
|
|
8953
|
-
const kernelApiKey = browserServiceApiKey();
|
|
8954
|
-
if (!kernelApiKey) return direct;
|
|
8955
|
-
const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
|
|
8956
|
-
if (!kernelProxyId) return direct;
|
|
8957
|
-
try {
|
|
8958
|
-
console.warn(`[CaptionFetcher] ${videoId}: retrying through residential proxy after bot block`);
|
|
8959
|
-
const viaProxy = await probeAndExtract(videoId, { ...opts, kernelProxyId });
|
|
8960
|
-
return viaProxy ?? direct;
|
|
8961
|
-
} finally {
|
|
8962
|
-
if (disposableProxyId) {
|
|
8963
|
-
await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
|
|
8964
|
-
(err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
|
|
8965
|
-
);
|
|
8966
|
-
}
|
|
8967
|
-
}
|
|
8968
|
-
}
|
|
8969
8953
|
async function fetchViaKernelSession(videoId) {
|
|
8970
8954
|
const start = Date.now();
|
|
8971
|
-
|
|
8972
|
-
if (!outcome)
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
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
|
+
}
|
|
8986
8973
|
}
|
|
8987
|
-
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" };
|
|
8981
|
+
}
|
|
8982
|
+
async function fetchViaResolvedAudio(videoId) {
|
|
8983
|
+
if (!mediaResolverConfigured()) return null;
|
|
8984
|
+
return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "audio-whisper" }, async () => {
|
|
8985
|
+
const resolved = await resolveYouTubeAudio(videoId);
|
|
8986
|
+
if (!resolved) return null;
|
|
8987
|
+
try {
|
|
8988
|
+
const transcript = await transcribeMediaUrl(resolved.mediaUrl, `# Transcript \u2014 ${videoId}`);
|
|
8989
|
+
if (!transcript.text) return null;
|
|
8990
|
+
const chunks = transcript.chunks.map((c) => ({ timestamp: c.timestamp, text: c.text.trim() }));
|
|
8991
|
+
return {
|
|
8992
|
+
videoId,
|
|
8993
|
+
text: transcript.text,
|
|
8994
|
+
chunks,
|
|
8995
|
+
durationMs: resolved.durationS > 0 ? Math.round(resolved.durationS * 1e3) : transcript.durationMs,
|
|
8996
|
+
method: "audio-whisper"
|
|
8997
|
+
};
|
|
8998
|
+
} catch (err) {
|
|
8999
|
+
console.warn(`[CaptionFetcher] transcription of resolved audio failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
9000
|
+
return null;
|
|
9001
|
+
}
|
|
9002
|
+
});
|
|
8988
9003
|
}
|
|
8989
9004
|
function finalizeCaptions(result) {
|
|
8990
9005
|
const chunks = result.chunks.map((c) => {
|
|
@@ -9006,8 +9021,10 @@ async function fetchCaptions(videoId) {
|
|
|
9006
9021
|
if (primary) return finalizeCaptions(primary);
|
|
9007
9022
|
const result = await fetchViaKernelSession(videoId);
|
|
9008
9023
|
if (result) return finalizeCaptions(result);
|
|
9024
|
+
const viaResolvedAudio = await fetchViaResolvedAudio(videoId);
|
|
9025
|
+
if (viaResolvedAudio) return finalizeCaptions(viaResolvedAudio);
|
|
9009
9026
|
throw new Error(
|
|
9010
|
-
`Could not transcribe ${videoId}
|
|
9027
|
+
`Could not transcribe ${videoId}: it has no caption track and its audio could not be retrieved.`
|
|
9011
9028
|
);
|
|
9012
9029
|
}
|
|
9013
9030
|
|
|
@@ -10373,42 +10390,7 @@ async function extractFacebookOrganicVideoFromPage(page, sourceUrl, quality = "b
|
|
|
10373
10390
|
}
|
|
10374
10391
|
|
|
10375
10392
|
// src/api/facebook-ad-routes.ts
|
|
10376
|
-
import { fal as fal3 } from "@fal-ai/client";
|
|
10377
|
-
|
|
10378
|
-
// src/services/media-transcription.ts
|
|
10379
10393
|
import { fal as fal2 } from "@fal-ai/client";
|
|
10380
|
-
function transcriptMarkdown(title, text, chunks, durationMs) {
|
|
10381
|
-
const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
|
|
10382
|
-
const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
|
|
10383
|
-
if (chunks.length) {
|
|
10384
|
-
lines.push("## Timestamped Segments", "");
|
|
10385
|
-
for (const ch of chunks) {
|
|
10386
|
-
lines.push(`**[${fmtTs2(ch.timestamp[0])} -> ${fmtTs2(ch.timestamp[1])}]** ${ch.text.trim()}`, "");
|
|
10387
|
-
}
|
|
10388
|
-
}
|
|
10389
|
-
return lines.join("\n");
|
|
10390
|
-
}
|
|
10391
|
-
async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript") {
|
|
10392
|
-
const startMs = Date.now();
|
|
10393
|
-
const result = await fal2.subscribe("fal-ai/wizper", {
|
|
10394
|
-
input: { audio_url: mediaUrl, task: "transcribe", language: "en" },
|
|
10395
|
-
logs: false,
|
|
10396
|
-
pollInterval: 3e3
|
|
10397
|
-
});
|
|
10398
|
-
const data = result.data;
|
|
10399
|
-
const text = data.text ?? "";
|
|
10400
|
-
const chunks = data.chunks ?? [];
|
|
10401
|
-
const durationMs = Date.now() - startMs;
|
|
10402
|
-
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: durationMs / 1e3, unitType: "compute_sec" });
|
|
10403
|
-
return {
|
|
10404
|
-
text,
|
|
10405
|
-
chunks,
|
|
10406
|
-
durationMs,
|
|
10407
|
-
markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs)
|
|
10408
|
-
};
|
|
10409
|
-
}
|
|
10410
|
-
|
|
10411
|
-
// src/api/facebook-ad-routes.ts
|
|
10412
10394
|
var FacebookAdBodySchema = z13.object({
|
|
10413
10395
|
url: z13.string().trim().optional(),
|
|
10414
10396
|
libraryId: z13.string().trim().optional(),
|
|
@@ -10588,17 +10570,17 @@ facebookAdApp.post("/transcribe", createApiKeyAuth(), async (c) => {
|
|
|
10588
10570
|
metadata: { videoUrl }
|
|
10589
10571
|
});
|
|
10590
10572
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
10591
|
-
|
|
10592
|
-
const holdMc =
|
|
10573
|
+
fal2.config({ credentials: process.env.FAL_KEY });
|
|
10574
|
+
const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
|
|
10593
10575
|
let debited = false;
|
|
10594
10576
|
try {
|
|
10595
10577
|
const { ok, balance_mc } = await debitMc(fbUser.id, holdMc, LedgerOperation.FB_TRANSCRIBE, videoUrl);
|
|
10596
10578
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
|
|
10597
10579
|
debited = true;
|
|
10598
10580
|
const transcript = await transcribeFacebookVideoUrl(videoUrl, "# Facebook Ad Transcript");
|
|
10599
|
-
const
|
|
10600
|
-
const diff = holdMc -
|
|
10601
|
-
if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "
|
|
10581
|
+
const settleMc = Math.round(transcript.costUsd * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
|
|
10582
|
+
const diff = holdMc - settleMc;
|
|
10583
|
+
if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
10602
10584
|
else if (diff < 0) await debitMc(fbUser.id, -diff, LedgerOperation.FB_TRANSCRIBE, videoUrl);
|
|
10603
10585
|
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 } });
|
|
10604
10586
|
return c.json(transcript);
|
|
@@ -10632,22 +10614,24 @@ facebookAdApp.post("/video-transcribe", createApiKeyAuth(), async (c) => {
|
|
|
10632
10614
|
metadata: { url: sourceUrl.href }
|
|
10633
10615
|
});
|
|
10634
10616
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
10635
|
-
|
|
10617
|
+
fal2.config({ credentials: process.env.FAL_KEY });
|
|
10636
10618
|
const driver = new BrowserDriver();
|
|
10637
|
-
const holdMc =
|
|
10619
|
+
const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
|
|
10638
10620
|
let debited = false;
|
|
10639
10621
|
try {
|
|
10640
10622
|
const { ok, balance_mc } = await debitMc(fbUser.id, holdMc, LedgerOperation.FB_TRANSCRIBE, sourceUrl.href);
|
|
10641
10623
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
|
|
10642
10624
|
debited = true;
|
|
10625
|
+
const kernelStartMs = Date.now();
|
|
10643
10626
|
await driver.launch(await kernelLaunchOptsResidential());
|
|
10644
10627
|
await driver.navigateTo(sourceUrl.href);
|
|
10645
10628
|
const page = driver.getPage();
|
|
10646
10629
|
const video = await extractFacebookOrganicVideoFromPage(page, sourceUrl.href, body.quality);
|
|
10630
|
+
const kernelMs = Date.now() - kernelStartMs;
|
|
10647
10631
|
const transcript = await transcribeFacebookVideoUrl(video.videoUrl, "# Facebook Organic Video Transcript");
|
|
10648
|
-
const
|
|
10649
|
-
const diff = holdMc -
|
|
10650
|
-
if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "
|
|
10632
|
+
const settleMc = Math.round((transcript.costUsd + kernelCostUsd(kernelMs, true)) * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
|
|
10633
|
+
const diff = holdMc - settleMc;
|
|
10634
|
+
if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
10651
10635
|
else if (diff < 0) await debitMc(fbUser.id, -diff, LedgerOperation.FB_TRANSCRIBE, sourceUrl.href);
|
|
10652
10636
|
const result = {
|
|
10653
10637
|
...video,
|
|
@@ -10983,7 +10967,7 @@ var GoogleAdsExtractor = class {
|
|
|
10983
10967
|
};
|
|
10984
10968
|
|
|
10985
10969
|
// src/api/google-ads-routes.ts
|
|
10986
|
-
import { fal as
|
|
10970
|
+
import { fal as fal3 } from "@fal-ai/client";
|
|
10987
10971
|
var GoogleAdsSearchBodySchema = z14.object({
|
|
10988
10972
|
query: z14.string().trim().min(1, "query is required"),
|
|
10989
10973
|
region: z14.string().trim().toUpperCase().length(2).optional(),
|
|
@@ -11039,9 +11023,11 @@ googleAdsApp.post("/search", createApiKeyAuth(), async (c) => {
|
|
|
11039
11023
|
const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH, body.query.trim());
|
|
11040
11024
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.google_ads_search), 402);
|
|
11041
11025
|
debited = true;
|
|
11042
|
-
await
|
|
11043
|
-
|
|
11044
|
-
|
|
11026
|
+
const result = await runWithCostContext({ ...currentCostContext(), op: "google_ads_search", userId: user.id }, async () => {
|
|
11027
|
+
await driver.launch(await kernelLaunchOptsResidential2());
|
|
11028
|
+
const extractor = new GoogleAdsExtractor(driver);
|
|
11029
|
+
return extractor.search(body.query.trim(), region, maxResults);
|
|
11030
|
+
});
|
|
11045
11031
|
if (result.advertisers.length === 0) {
|
|
11046
11032
|
await creditMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH_REFUND, "empty result");
|
|
11047
11033
|
refunded = true;
|
|
@@ -11077,9 +11063,11 @@ googleAdsApp.post("/page-intel", createApiKeyAuth(), async (c) => {
|
|
|
11077
11063
|
const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL, queryLabel);
|
|
11078
11064
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.google_ads_intel), 402);
|
|
11079
11065
|
debited = true;
|
|
11080
|
-
await
|
|
11081
|
-
|
|
11082
|
-
|
|
11066
|
+
const result = await runWithCostContext({ ...currentCostContext(), op: "google_ads_intel", userId: user.id }, async () => {
|
|
11067
|
+
await driver.launch(await kernelLaunchOptsResidential2());
|
|
11068
|
+
const extractor = new GoogleAdsExtractor(driver);
|
|
11069
|
+
return extractor.pageIntel({ advertiserId: body.advertiserId, domain: body.domain, region, maxAds });
|
|
11070
|
+
});
|
|
11083
11071
|
if (result.ads.length === 0) {
|
|
11084
11072
|
await creditMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL_REFUND, "empty result");
|
|
11085
11073
|
refunded = true;
|
|
@@ -11109,17 +11097,17 @@ googleAdsApp.post("/transcribe", createApiKeyAuth(), async (c) => {
|
|
|
11109
11097
|
const user = c.get("user");
|
|
11110
11098
|
const gate = await acquireConcurrencyGate(user, "google_ads_transcribe", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { videoUrl } });
|
|
11111
11099
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
11112
|
-
|
|
11113
|
-
const holdMc =
|
|
11100
|
+
fal3.config({ credentials: process.env.FAL_KEY });
|
|
11101
|
+
const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
|
|
11114
11102
|
let debited = false;
|
|
11115
11103
|
try {
|
|
11116
11104
|
const { ok, balance_mc } = await debitMc(user.id, holdMc, LedgerOperation.GOOGLE_ADS_TRANSCRIBE, videoUrl);
|
|
11117
11105
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
|
|
11118
11106
|
debited = true;
|
|
11119
|
-
const transcript = await transcribeMediaUrl(videoUrl, "# Google Ad Transcript");
|
|
11120
|
-
const
|
|
11121
|
-
const diff = holdMc -
|
|
11122
|
-
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE_REFUND, "
|
|
11107
|
+
const transcript = await runWithCostContext({ ...currentCostContext(), op: "google_ads_transcribe", userId: user.id }, () => transcribeMediaUrl(videoUrl, "# Google Ad Transcript"));
|
|
11108
|
+
const settleMc = Math.round(transcript.costUsd * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
|
|
11109
|
+
const diff = holdMc - settleMc;
|
|
11110
|
+
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
11123
11111
|
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE, videoUrl);
|
|
11124
11112
|
await logRequestEvent({ userId: user.id, source: "google_ads_transcribe", status: "done", query: videoUrl, resultCount: transcript.chunks.length, result: { text: transcript.text, chunks: transcript.chunks, durationMs: transcript.durationMs } });
|
|
11125
11113
|
return c.json(transcript);
|
|
@@ -11768,10 +11756,15 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
|
|
|
11768
11756
|
if (!audioUrl) {
|
|
11769
11757
|
warnings.push("Transcript requested, but no audio/video track was available.");
|
|
11770
11758
|
} else {
|
|
11771
|
-
const
|
|
11772
|
-
|
|
11759
|
+
const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
|
|
11760
|
+
const tx = await debitMc(user.id, holdMc, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
|
|
11761
|
+
if (!tx.ok) return c.json(insufficientBalanceResponse(tx.balance_mc, holdMc), 402);
|
|
11773
11762
|
transcriptDebited = true;
|
|
11774
11763
|
transcript = await transcribeMediaUrl(audioUrl, "# Instagram Media Transcript");
|
|
11764
|
+
const settleMc = Math.round(transcript.costUsd * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
|
|
11765
|
+
const diff = holdMc - settleMc;
|
|
11766
|
+
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
11767
|
+
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
|
|
11775
11768
|
}
|
|
11776
11769
|
}
|
|
11777
11770
|
const result = {
|
|
@@ -11786,7 +11779,7 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
|
|
|
11786
11779
|
return c.json(result);
|
|
11787
11780
|
} catch (err) {
|
|
11788
11781
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11789
|
-
if (transcriptDebited) await creditMc(user.id,
|
|
11782
|
+
if (transcriptDebited) await creditMc(user.id, MEDIA_TRANSCRIBE_HOLD_MC, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "failed call");
|
|
11790
11783
|
if (mediaDebited) await creditMc(user.id, MC_COSTS.instagram_media, LedgerOperation.INSTAGRAM_MEDIA_REFUND, "failed call");
|
|
11791
11784
|
await logRequestEvent({ userId: user.id, source: "instagram_media_download", status: "failed", query: sourceUrl.href, error: msg });
|
|
11792
11785
|
if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) return c.json({ error: msg }, 503);
|
|
@@ -11914,10 +11907,23 @@ redditApp.post("/thread", createApiKeyAuth(), async (c) => {
|
|
|
11914
11907
|
result.comments = result.comments.slice(0, body.maxComments);
|
|
11915
11908
|
result.numComments = result.comments.length;
|
|
11916
11909
|
}
|
|
11910
|
+
const commentCount = Array.isArray(result.comments) ? result.comments.length : 0;
|
|
11911
|
+
commentDebitMc = commentCount * MC_COSTS.reddit_comment;
|
|
11912
|
+
if (commentDebitMc > 0) {
|
|
11913
|
+
const commentDebit = await debitMc(user.id, commentDebitMc, LedgerOperation.REDDIT_COMMENT, body.url);
|
|
11914
|
+
if (!commentDebit.ok) {
|
|
11915
|
+
const affordable = Math.floor((commentDebit.balance_mc ?? 0) / MC_COSTS.reddit_comment);
|
|
11916
|
+
result.comments = result.comments.slice(0, affordable);
|
|
11917
|
+
result.numComments = affordable;
|
|
11918
|
+
commentDebitMc = affordable * MC_COSTS.reddit_comment;
|
|
11919
|
+
if (commentDebitMc > 0) await debitMc(user.id, commentDebitMc, LedgerOperation.REDDIT_COMMENT, body.url);
|
|
11920
|
+
}
|
|
11921
|
+
}
|
|
11917
11922
|
await logRequestEvent({ userId: user.id, source: "reddit_thread", status: "done", query: body.url, resultCount: Number(result.numComments ?? 0), result });
|
|
11918
11923
|
return c.json(result);
|
|
11919
11924
|
} catch (err) {
|
|
11920
11925
|
if (debited && !refunded) await creditMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD_REFUND, "failed call");
|
|
11926
|
+
if (commentDebitMc > 0) await creditMc(user.id, commentDebitMc, LedgerOperation.REDDIT_COMMENT_REFUND, "failed call");
|
|
11921
11927
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11922
11928
|
await logRequestEvent({ userId: user.id, source: "reddit_thread", status: "failed", query: body.url, error: msg });
|
|
11923
11929
|
return c.json({ error: msg }, 500);
|
|
@@ -20408,6 +20414,11 @@ function opForCostPath(p) {
|
|
|
20408
20414
|
return "fb_ad";
|
|
20409
20415
|
}
|
|
20410
20416
|
if (p.startsWith("/instagram/")) return "instagram";
|
|
20417
|
+
if (p.startsWith("/google-ads/")) {
|
|
20418
|
+
if (p.endsWith("/transcribe")) return "google_ads_transcribe";
|
|
20419
|
+
if (p.endsWith("/search")) return "google_ads_search";
|
|
20420
|
+
return "google_ads_intel";
|
|
20421
|
+
}
|
|
20411
20422
|
if (p === "/reddit/thread") return "reddit_thread";
|
|
20412
20423
|
if (p.startsWith("/trustpilot/")) return "trustpilot_reviews";
|
|
20413
20424
|
if (p.startsWith("/g2/")) return "g2_reviews";
|
|
@@ -21818,7 +21829,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
21818
21829
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
21819
21830
|
return c.json({ error: "Unauthorized" }, 401);
|
|
21820
21831
|
}
|
|
21821
|
-
const { drainQueue } = await import("./worker-
|
|
21832
|
+
const { drainQueue } = await import("./worker-WCSKUAOT.js");
|
|
21822
21833
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
21823
21834
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
21824
21835
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup] = await Promise.all([
|
|
@@ -21836,7 +21847,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
21836
21847
|
return c.json({ error: "Unauthorized" }, 401);
|
|
21837
21848
|
}
|
|
21838
21849
|
const jobId = c.req.param("id");
|
|
21839
|
-
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-
|
|
21850
|
+
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-6X2C7RLF.js");
|
|
21840
21851
|
const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
|
|
21841
21852
|
const job = await getExtractJob2(jobId);
|
|
21842
21853
|
if (!job) return c.json({ error: "job not found" }, 404);
|
|
@@ -21844,7 +21855,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
21844
21855
|
await completeExtractJob2(jobId, stored);
|
|
21845
21856
|
let settlement = "already_settled_or_refunded";
|
|
21846
21857
|
if (job.billedMc == null && job.userId != null) {
|
|
21847
|
-
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-
|
|
21858
|
+
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-6X2C7RLF.js");
|
|
21848
21859
|
const heldMc = Number(job.options.heldMc ?? 0);
|
|
21849
21860
|
const successful = await countSuccessfulPages2(jobId);
|
|
21850
21861
|
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
@@ -22008,4 +22019,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
22008
22019
|
export {
|
|
22009
22020
|
app
|
|
22010
22021
|
};
|
|
22011
|
-
//# sourceMappingURL=server-
|
|
22022
|
+
//# sourceMappingURL=server-TGORO4AJ.js.map
|