mcp-scraper 0.6.0 → 0.8.0
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 +3771 -3613
- 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 +3 -3
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +4 -4
- package/dist/bin/paa-harvest.cjs +166 -74
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/chunk-3MKSXDJ7.js +7 -0
- package/dist/chunk-3MKSXDJ7.js.map +1 -0
- package/dist/{chunk-APJO2XV5.js → chunk-HL33CGJF.js} +3 -3
- package/dist/{chunk-APJO2XV5.js.map → chunk-HL33CGJF.js.map} +1 -1
- package/dist/{chunk-3K5GT6VG.js → chunk-ITJC3NN4.js} +5 -4
- package/dist/{chunk-GWRIO6JT.js → chunk-R7EETU7Z.js} +134 -134
- package/dist/chunk-R7EETU7Z.js.map +1 -0
- package/dist/{chunk-AUCXKRRH.js → chunk-RMPPYKUV.js} +167 -75
- package/dist/chunk-RMPPYKUV.js.map +1 -0
- package/dist/{chunk-BL7BBSYF.js → chunk-RUGJE5EB.js} +2 -2
- package/dist/{extract-bundle-COS56ZDO.js → extract-bundle-U4D5LW5W.js} +4 -2
- package/dist/extract-bundle-U4D5LW5W.js.map +1 -0
- package/dist/index.cjs +166 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{server-WHM446IZ.js → server-WBDVVHR6.js} +151 -96
- package/dist/server-WBDVVHR6.js.map +1 -0
- package/dist/{site-extract-repository-U476J44K.js → site-extract-repository-NVSZH35Y.js} +3 -3
- package/dist/{worker-KJZ3ZN2N.js → worker-JQTS437L.js} +4 -4
- package/dist/worker-JQTS437L.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-AUCXKRRH.js.map +0 -1
- package/dist/chunk-F2BXETDX.js +0 -7
- package/dist/chunk-F2BXETDX.js.map +0 -1
- package/dist/chunk-GWRIO6JT.js.map +0 -1
- package/dist/extract-bundle-COS56ZDO.js.map +0 -1
- package/dist/server-WHM446IZ.js.map +0 -1
- package/dist/worker-KJZ3ZN2N.js.map +0 -1
- package/docs/spec-browser-routing.md +0 -108
- package/docs/spec-kernel-computer-controls.md +0 -166
- package/docs/spec-mcp-tools.md +0 -146
- package/docs/spec-server-instructions.md +0 -94
- package/docs/specs/agent-ready-seo-packet-spec.md +0 -237
- package/docs/specs/audit-visual-demo.md +0 -309
- package/docs/specs/cli-agent-wiring-spec.md +0 -206
- package/docs/specs/local-competitive-audit-spec.md +0 -311
- package/docs/specs/scheduled-workflows-api-spec.md +0 -304
- package/docs/specs/seo-cli-growth-roadmap-spec.md +0 -181
- package/docs/specs/seo-workflow-runner-and-reports-spec.md +0 -241
- /package/dist/{chunk-3K5GT6VG.js.map → chunk-ITJC3NN4.js.map} +0 -0
- /package/dist/{chunk-BL7BBSYF.js.map → chunk-RUGJE5EB.js.map} +0 -0
- /package/dist/{site-extract-repository-U476J44K.js.map → site-extract-repository-NVSZH35Y.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -18,10 +18,11 @@ import {
|
|
|
18
18
|
resolveKernelProxyId,
|
|
19
19
|
runWithCostContext,
|
|
20
20
|
vendorCostUsd
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-RMPPYKUV.js";
|
|
22
22
|
import {
|
|
23
23
|
HttpMcpToolExecutor,
|
|
24
24
|
MemoryMcpToolExecutor,
|
|
25
|
+
buildLinkGraph,
|
|
25
26
|
buildPaaExtractorMcpServer,
|
|
26
27
|
configureReportSaving,
|
|
27
28
|
exportFanout,
|
|
@@ -33,11 +34,13 @@ import {
|
|
|
33
34
|
registerSerpIntelligenceCaptureTools,
|
|
34
35
|
sanitizeAttempts,
|
|
35
36
|
sanitizeHarvestResult
|
|
36
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-ITJC3NN4.js";
|
|
37
38
|
import {
|
|
38
39
|
auditImages,
|
|
40
|
+
buildLinkReport,
|
|
41
|
+
computeIssues,
|
|
39
42
|
getBlobStore
|
|
40
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-R7EETU7Z.js";
|
|
41
44
|
import {
|
|
42
45
|
browserServiceApiKey,
|
|
43
46
|
browserServiceProfileName,
|
|
@@ -67,7 +70,7 @@ import {
|
|
|
67
70
|
RawMapsOverviewSchema,
|
|
68
71
|
RawMapsReviewStatsSchema
|
|
69
72
|
} from "./chunk-XGIPATLV.js";
|
|
70
|
-
import "./chunk-
|
|
73
|
+
import "./chunk-3MKSXDJ7.js";
|
|
71
74
|
import {
|
|
72
75
|
completeExtractJob,
|
|
73
76
|
countSuccessfulPages,
|
|
@@ -78,7 +81,7 @@ import {
|
|
|
78
81
|
saveExtractPages,
|
|
79
82
|
setExtractJobTotal,
|
|
80
83
|
settleExtractJob
|
|
81
|
-
} from "./chunk-
|
|
84
|
+
} from "./chunk-RUGJE5EB.js";
|
|
82
85
|
import {
|
|
83
86
|
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
84
87
|
CONCURRENCY_PRICE_ID,
|
|
@@ -101,7 +104,7 @@ import {
|
|
|
101
104
|
browserActiveCostMc,
|
|
102
105
|
concurrencySlotBillingInfo,
|
|
103
106
|
insufficientBalanceResponse
|
|
104
|
-
} from "./chunk-
|
|
107
|
+
} from "./chunk-HL33CGJF.js";
|
|
105
108
|
import {
|
|
106
109
|
CaptchaError,
|
|
107
110
|
RECAPTCHA_INSTRUCTIONS,
|
|
@@ -4432,8 +4435,8 @@ async function downloadAsset(url, destDir, filename) {
|
|
|
4432
4435
|
}
|
|
4433
4436
|
const writer = createWriteStream(dest);
|
|
4434
4437
|
await pipeline(Readable.fromWeb(res.body), writer);
|
|
4435
|
-
const { statSync
|
|
4436
|
-
const sizeBytes =
|
|
4438
|
+
const { statSync } = await import("fs");
|
|
4439
|
+
const sizeBytes = statSync(dest).size;
|
|
4437
4440
|
return { savedPath: dest, sizeBytes, mimeType };
|
|
4438
4441
|
}
|
|
4439
4442
|
async function harvestPageMedia(html, pageUrl, options = {}) {
|
|
@@ -4780,7 +4783,8 @@ async function spiderSite(opts) {
|
|
|
4780
4783
|
totalFound: results.length,
|
|
4781
4784
|
durationMs: Date.now() - startMs,
|
|
4782
4785
|
truncated: queue.length > 0 || results.length >= maxUrls,
|
|
4783
|
-
browserRetries
|
|
4786
|
+
browserRetries,
|
|
4787
|
+
sitemapUrls: sitemapUrls.slice(0, maxUrls)
|
|
4784
4788
|
};
|
|
4785
4789
|
}
|
|
4786
4790
|
|
|
@@ -4904,6 +4908,23 @@ async function crawlWithRotation(urls, opts) {
|
|
|
4904
4908
|
return urls.map((u) => byUrl.get(u) ?? { url: u, html: null, status: 0, via: "browser" });
|
|
4905
4909
|
}
|
|
4906
4910
|
|
|
4911
|
+
// src/api/seo-audit.ts
|
|
4912
|
+
function buildSeoAudit(pages, startUrl) {
|
|
4913
|
+
const { edges, metrics } = buildLinkGraph(pages, startUrl);
|
|
4914
|
+
const issues = computeIssues(pages, metrics);
|
|
4915
|
+
const { summary } = buildLinkReport(edges, [...metrics.values()], startUrl);
|
|
4916
|
+
return {
|
|
4917
|
+
issues,
|
|
4918
|
+
linkSummary: summary,
|
|
4919
|
+
pageMetrics: [...metrics.values()].map((m) => ({
|
|
4920
|
+
url: m.url,
|
|
4921
|
+
inboundInternal: m.inlinks,
|
|
4922
|
+
outboundInternal: m.outlinksInternal,
|
|
4923
|
+
orphan: m.orphan
|
|
4924
|
+
}))
|
|
4925
|
+
};
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4907
4928
|
// src/api/site-extractor.ts
|
|
4908
4929
|
var PIXEL_WIDTHS = { i: 4, l: 4, j: 4, ".": 4, ",": 4, "'": 4, t: 6, f: 6, r: 6, " ": 4, m: 14, w: 13, W: 16, M: 16 };
|
|
4909
4930
|
function estimatePixels(s) {
|
|
@@ -4947,6 +4968,7 @@ function emptyPageData(url, status, via) {
|
|
|
4947
4968
|
imageLinks: [],
|
|
4948
4969
|
indexable: status === 200,
|
|
4949
4970
|
indexabilityReason: status === 200 ? null : "non-200",
|
|
4971
|
+
inSitemap: null,
|
|
4950
4972
|
schemaTypes: [],
|
|
4951
4973
|
canonicalUrl: null,
|
|
4952
4974
|
internalLinks: 0,
|
|
@@ -4963,7 +4985,8 @@ function makeSeedSpider(startUrl, urls) {
|
|
|
4963
4985
|
totalFound: urls.length,
|
|
4964
4986
|
durationMs: 0,
|
|
4965
4987
|
truncated: false,
|
|
4966
|
-
browserRetries: 0
|
|
4988
|
+
browserRetries: 0,
|
|
4989
|
+
sitemapUrls: []
|
|
4967
4990
|
};
|
|
4968
4991
|
}
|
|
4969
4992
|
var UA2 = "Mozilla/5.0 (compatible; ThorbitBot/1.0; +https://thorbit.ai)";
|
|
@@ -5134,6 +5157,7 @@ function parsePageData(url, html, status, via, resp) {
|
|
|
5134
5157
|
imageLinks,
|
|
5135
5158
|
indexable,
|
|
5136
5159
|
indexabilityReason,
|
|
5160
|
+
inSitemap: null,
|
|
5137
5161
|
schemaTypes,
|
|
5138
5162
|
canonicalUrl,
|
|
5139
5163
|
internalLinks,
|
|
@@ -5147,11 +5171,11 @@ async function extractPagesRotating(urls, opts) {
|
|
|
5147
5171
|
const fetched = await crawlWithRotation(urls, { apiKey: opts.kernelApiKey, concurrency: opts.concurrency, urlsPerBrowser: opts.urlsPerBrowser });
|
|
5148
5172
|
return fetched.map((r) => r.html ? parsePageData(r.url, r.html, r.status || 200, "browser", { headers: r.headers, responseTimeMs: r.responseTimeMs, redirectUrl: r.redirectUrl }) : emptyPageData(r.url, r.status || null, "browser"));
|
|
5149
5173
|
}
|
|
5150
|
-
async function
|
|
5174
|
+
async function discoverUrlsWithSitemap(startUrl, maxPages, kernelApiKey) {
|
|
5151
5175
|
const sitemapUrls = await discoverSitemapUrls2(startUrl, maxPages);
|
|
5152
|
-
if (sitemapUrls.length > 0) return sitemapUrls;
|
|
5176
|
+
if (sitemapUrls.length > 0) return { urls: sitemapUrls, sitemapUrls };
|
|
5153
5177
|
const spider = await spiderSite({ startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey });
|
|
5154
|
-
return spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
|
|
5178
|
+
return { urls: spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url), sitemapUrls: spider.sitemapUrls };
|
|
5155
5179
|
}
|
|
5156
5180
|
async function fetchAndParse(url, kernelApiKey) {
|
|
5157
5181
|
const startedAt = Date.now();
|
|
@@ -5210,6 +5234,7 @@ async function extractSite(opts) {
|
|
|
5210
5234
|
let browserRetries = 0;
|
|
5211
5235
|
let truncated = false;
|
|
5212
5236
|
let spider;
|
|
5237
|
+
let sitemapUrls = [];
|
|
5213
5238
|
if (rotating) {
|
|
5214
5239
|
const browserConcurrency = Math.max(1, Math.min(opts.parallelism ?? 1, 8));
|
|
5215
5240
|
const urlsPerBrowser = opts.rotateProxyEvery ?? 10;
|
|
@@ -5227,7 +5252,8 @@ async function extractSite(opts) {
|
|
|
5227
5252
|
for (const u of opts.seedUrls) enqueue(u);
|
|
5228
5253
|
} else {
|
|
5229
5254
|
enqueue(opts.startUrl);
|
|
5230
|
-
|
|
5255
|
+
sitemapUrls = await discoverSitemapUrls2(opts.startUrl, maxPages);
|
|
5256
|
+
for (const u of sitemapUrls) enqueue(u);
|
|
5231
5257
|
}
|
|
5232
5258
|
const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
|
|
5233
5259
|
let done = 0;
|
|
@@ -5256,6 +5282,7 @@ async function extractSite(opts) {
|
|
|
5256
5282
|
spider = makeSeedSpider(opts.startUrl, [...seen]);
|
|
5257
5283
|
} else {
|
|
5258
5284
|
spider = seeded ? makeSeedSpider(opts.startUrl, opts.seedUrls) : await spiderSite({ startUrl: opts.startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey: opts.kernelApiKey });
|
|
5285
|
+
sitemapUrls = spider.sitemapUrls;
|
|
5259
5286
|
const urlsToExtract = spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
|
|
5260
5287
|
browserRetries = spider.browserRetries;
|
|
5261
5288
|
await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
|
|
@@ -5265,10 +5292,17 @@ async function extractSite(opts) {
|
|
|
5265
5292
|
});
|
|
5266
5293
|
truncated = spider.truncated || pages.length >= maxPages;
|
|
5267
5294
|
}
|
|
5295
|
+
const sitemapSet = sitemapUrls.length > 0 ? new Set(sitemapUrls.map((u) => normalizeUrl(u, opts.startUrl) ?? u)) : null;
|
|
5296
|
+
for (const p of pages) p.inSitemap = sitemapSet ? sitemapSet.has(normalizeUrl(p.url, opts.startUrl) ?? p.url) : null;
|
|
5268
5297
|
let branding = null;
|
|
5269
5298
|
if (opts.formats?.includes("branding") && opts.kernelApiKey) {
|
|
5270
5299
|
branding = await extractBranding(opts.startUrl, opts.kernelApiKey).catch(() => null);
|
|
5271
5300
|
}
|
|
5301
|
+
const seoAudit = opts.formats?.includes("issues") ? buildSeoAudit(pages, opts.startUrl) : null;
|
|
5302
|
+
let imageAudit = null;
|
|
5303
|
+
if (opts.formats?.includes("images")) {
|
|
5304
|
+
imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
|
|
5305
|
+
}
|
|
5272
5306
|
return {
|
|
5273
5307
|
startUrl: opts.startUrl,
|
|
5274
5308
|
pages,
|
|
@@ -5277,7 +5311,9 @@ async function extractSite(opts) {
|
|
|
5277
5311
|
durationMs: Date.now() - startMs,
|
|
5278
5312
|
truncated,
|
|
5279
5313
|
browserRetries,
|
|
5280
|
-
branding
|
|
5314
|
+
branding,
|
|
5315
|
+
seoAudit,
|
|
5316
|
+
imageAudit
|
|
5281
5317
|
};
|
|
5282
5318
|
}
|
|
5283
5319
|
|
|
@@ -7519,12 +7555,13 @@ var siteExtractFn = inngest.createFunction(
|
|
|
7519
7555
|
const maxPages = Number(job.options.maxPages ?? 1e4);
|
|
7520
7556
|
const concurrency = Number(job.options.concurrency ?? 1);
|
|
7521
7557
|
const urlsPerBrowser = Number(job.options.urlsPerBrowser ?? job.options.rotateProxyEvery ?? 10);
|
|
7522
|
-
const
|
|
7523
|
-
const discovered = await
|
|
7524
|
-
const
|
|
7525
|
-
await setExtractJobTotal(jobId,
|
|
7526
|
-
return
|
|
7558
|
+
const seedRaw = await step.run("discover", async () => {
|
|
7559
|
+
const discovered = await discoverUrlsWithSitemap(job.startUrl, maxPages, key);
|
|
7560
|
+
const urls = discovered.urls.length ? discovered.urls : [job.startUrl];
|
|
7561
|
+
await setExtractJobTotal(jobId, urls.length);
|
|
7562
|
+
return { urls, sitemapUrls: discovered.sitemapUrls };
|
|
7527
7563
|
});
|
|
7564
|
+
const seed = Array.isArray(seedRaw) ? { urls: seedRaw, sitemapUrls: [] } : seedRaw;
|
|
7528
7565
|
const siteHost = (() => {
|
|
7529
7566
|
try {
|
|
7530
7567
|
return new URL(job.startUrl).hostname.replace(/^www\./, "");
|
|
@@ -7548,8 +7585,9 @@ var siteExtractFn = inngest.createFunction(
|
|
|
7548
7585
|
return false;
|
|
7549
7586
|
}
|
|
7550
7587
|
};
|
|
7551
|
-
const
|
|
7552
|
-
const
|
|
7588
|
+
const sitemapSet = seed.sitemapUrls.length > 0 ? new Set(seed.sitemapUrls.map(norm)) : null;
|
|
7589
|
+
const queue = [...seed.urls];
|
|
7590
|
+
const seen = new Set(seed.urls.map(norm));
|
|
7553
7591
|
let crawled = 0;
|
|
7554
7592
|
const MAX_BATCHES = Math.ceil(maxPages / BATCH) + 30;
|
|
7555
7593
|
for (let i = 0; i < MAX_BATCHES && crawled < maxPages && crawled < queue.length; i++) {
|
|
@@ -7557,7 +7595,7 @@ var siteExtractFn = inngest.createFunction(
|
|
|
7557
7595
|
if (!batch.length) break;
|
|
7558
7596
|
const newLinks = await step.run(`crawl-batch-${i}`, async () => {
|
|
7559
7597
|
const pages = await extractPagesRotating(batch, { kernelApiKey: key, concurrency, urlsPerBrowser });
|
|
7560
|
-
await saveExtractPages(jobId, pages);
|
|
7598
|
+
await saveExtractPages(jobId, pages.map((p) => ({ ...p, inSitemap: sitemapSet ? sitemapSet.has(norm(p.url)) : null })));
|
|
7561
7599
|
const found = [];
|
|
7562
7600
|
for (const p of pages) for (const l of p.outlinks ?? []) {
|
|
7563
7601
|
if (l.internal && l.href && sameSite(l.href)) found.push(l.href);
|
|
@@ -7581,9 +7619,13 @@ var siteExtractFn = inngest.createFunction(
|
|
|
7581
7619
|
const pages = await getExtractedPages(jobId);
|
|
7582
7620
|
return auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
|
|
7583
7621
|
}) : null;
|
|
7622
|
+
const seoAudit = Array.isArray(job.options.formats) && job.options.formats.includes("issues") ? await step.run("seo-audit", async () => {
|
|
7623
|
+
const pages = await getExtractedPages(jobId);
|
|
7624
|
+
return buildSeoAudit(pages, job.startUrl);
|
|
7625
|
+
}) : null;
|
|
7584
7626
|
const artifacts = await step.run("finalize", async () => {
|
|
7585
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
7586
|
-
const stored = await assembleExtractArtifacts(job, { branding, imageAudit });
|
|
7627
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-U4D5LW5W.js");
|
|
7628
|
+
const stored = await assembleExtractArtifacts(job, { branding, imageAudit, seoAudit });
|
|
7587
7629
|
await completeExtractJob(jobId, stored);
|
|
7588
7630
|
return stored;
|
|
7589
7631
|
});
|
|
@@ -9089,7 +9131,7 @@ var ExtractSiteBodySchema = z11.object({
|
|
|
9089
9131
|
rotateProxies: z11.boolean().optional(),
|
|
9090
9132
|
rotateProxyEvery: z11.number().int().min(1).max(100).optional(),
|
|
9091
9133
|
background: z11.boolean().optional(),
|
|
9092
|
-
formats: z11.array(z11.enum(["markdown", "links", "json", "images", "branding"])).optional()
|
|
9134
|
+
formats: z11.array(z11.enum(["markdown", "links", "json", "images", "branding", "issues"])).optional()
|
|
9093
9135
|
});
|
|
9094
9136
|
var YoutubeHarvestBodySchema = z11.object({
|
|
9095
9137
|
mode: z11.enum(["search", "channel"]),
|
|
@@ -10734,6 +10776,12 @@ facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
|
|
|
10734
10776
|
}
|
|
10735
10777
|
}
|
|
10736
10778
|
const results = [...advertiserMap.values()].sort((a, b) => b.adCount - a.adCount).slice(0, maxResults).map((a) => ({ name: a.pageName, pageName: a.pageName, libraryId: a.sampleLibraryId, sampleLibraryId: a.sampleLibraryId, adCount: a.adCount }));
|
|
10779
|
+
if (results.length === 0) {
|
|
10780
|
+
await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "empty result");
|
|
10781
|
+
searchRefunded = true;
|
|
10782
|
+
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
|
|
10783
|
+
return c.json({ error: "no advertisers found (refunded)" }, 503);
|
|
10784
|
+
}
|
|
10737
10785
|
const searchResult = { query: body.query.trim(), searchUrl, results };
|
|
10738
10786
|
await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results.length, result: searchResult });
|
|
10739
10787
|
return c.json(searchResult);
|
|
@@ -11124,11 +11172,9 @@ googleAdsApp.post("/transcribe", createApiKeyAuth(), async (c) => {
|
|
|
11124
11172
|
// src/api/instagram-routes.ts
|
|
11125
11173
|
import { Hono as Hono7 } from "hono";
|
|
11126
11174
|
import { z as z15 } from "zod";
|
|
11127
|
-
import {
|
|
11128
|
-
import {
|
|
11175
|
+
import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
11176
|
+
import { tmpdir } from "os";
|
|
11129
11177
|
import { join as join4 } from "path";
|
|
11130
|
-
import { Readable as Readable2 } from "stream";
|
|
11131
|
-
import { pipeline as pipeline2 } from "stream/promises";
|
|
11132
11178
|
import { spawn } from "child_process";
|
|
11133
11179
|
|
|
11134
11180
|
// src/extractor/InstagramContentExtractor.ts
|
|
@@ -11552,20 +11598,15 @@ async function resolveInstagramLaunch(body) {
|
|
|
11552
11598
|
}
|
|
11553
11599
|
};
|
|
11554
11600
|
}
|
|
11555
|
-
function outputBaseDir2() {
|
|
11556
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join4(homedir2(), "Downloads", "mcp-scraper");
|
|
11557
|
-
}
|
|
11558
11601
|
function safeFilePart(input) {
|
|
11559
11602
|
return input.replace(/^https?:\/\//, "").replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "instagram";
|
|
11560
11603
|
}
|
|
11561
|
-
function
|
|
11604
|
+
function blobKeyPrefix(shortcode, sourceUrl) {
|
|
11562
11605
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
11563
11606
|
const slug = shortcode ? `instagram-${shortcode}` : safeFilePart(sourceUrl);
|
|
11564
|
-
|
|
11565
|
-
mkdirSync2(dir, { recursive: true });
|
|
11566
|
-
return dir;
|
|
11607
|
+
return `instagram/${stamp}-${slug}`;
|
|
11567
11608
|
}
|
|
11568
|
-
async function
|
|
11609
|
+
async function fetchMediaBytes(url, referer) {
|
|
11569
11610
|
const res = await fetch(url, {
|
|
11570
11611
|
headers: {
|
|
11571
11612
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
@@ -11575,12 +11616,8 @@ async function downloadToFile(url, destPath, referer) {
|
|
|
11575
11616
|
});
|
|
11576
11617
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
11577
11618
|
if (!res.body) throw new Error("Empty response body");
|
|
11578
|
-
|
|
11579
|
-
return {
|
|
11580
|
-
savedPath: destPath,
|
|
11581
|
-
sizeBytes: statSync(destPath).size,
|
|
11582
|
-
mimeType: res.headers.get("content-type")?.split(";")[0]?.trim() ?? null
|
|
11583
|
-
};
|
|
11619
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
11620
|
+
return { bytes, mimeType: res.headers.get("content-type")?.split(";")[0]?.trim() ?? null };
|
|
11584
11621
|
}
|
|
11585
11622
|
function extFromMime(mimeType, fallback) {
|
|
11586
11623
|
const map = {
|
|
@@ -11699,55 +11736,65 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
|
|
|
11699
11736
|
const mediaTypes = new Set(body.mediaTypes);
|
|
11700
11737
|
const downloads = [];
|
|
11701
11738
|
const warnings = [];
|
|
11702
|
-
let outputDir = null;
|
|
11703
11739
|
if (body.downloadMedia) {
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
const
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
const
|
|
11721
|
-
|
|
11740
|
+
let muxWorkDir = null;
|
|
11741
|
+
try {
|
|
11742
|
+
const store = getBlobStore();
|
|
11743
|
+
const prefix = blobKeyPrefix(extraction.shortcode, sourceUrl.href);
|
|
11744
|
+
const textContent = [
|
|
11745
|
+
`URL: ${extraction.pageUrl}`,
|
|
11746
|
+
extraction.caption ? `Caption: ${extraction.caption}` : "",
|
|
11747
|
+
"",
|
|
11748
|
+
extraction.bodyText
|
|
11749
|
+
].filter(Boolean).join("\n");
|
|
11750
|
+
const storedText = await store.put(`${prefix}/text.txt`, textContent, "text/plain");
|
|
11751
|
+
downloads.push({ kind: "text", url: null, savedPath: storedText.url, sizeBytes: storedText.bytes, mimeType: "text/plain", error: null });
|
|
11752
|
+
if (mediaTypes.has("image") && extraction.imageUrl) {
|
|
11753
|
+
try {
|
|
11754
|
+
const { bytes, mimeType } = await fetchMediaBytes(extraction.imageUrl, extraction.pageUrl);
|
|
11755
|
+
const ext = extFromMime(mimeType, "jpg");
|
|
11756
|
+
const stored = await store.put(`${prefix}/image.${ext}`, bytes, mimeType ?? "application/octet-stream");
|
|
11757
|
+
downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: stored.url, sizeBytes: stored.bytes, mimeType, error: null });
|
|
11758
|
+
} catch (err) {
|
|
11759
|
+
downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
|
|
11722
11760
|
}
|
|
11723
|
-
downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: finalPath, sizeBytes: statSync(finalPath).size, mimeType: downloaded.mimeType, error: null });
|
|
11724
|
-
} catch (err) {
|
|
11725
|
-
downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
|
|
11726
11761
|
}
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11762
|
+
const tracksToDownload = body.downloadAllTracks ? extraction.tracks : [extraction.selectedVideoTrack, extraction.selectedAudioTrack].filter((track) => Boolean(track));
|
|
11763
|
+
const muxEligible = Boolean(body.mux && mediaTypes.has("video") && mediaTypes.has("audio") && extraction.selectedVideoTrack && extraction.selectedAudioTrack);
|
|
11764
|
+
const muxInputs = /* @__PURE__ */ new Map();
|
|
11765
|
+
for (const [index, track] of tracksToDownload.entries()) {
|
|
11766
|
+
if (track.streamType === "video" && !mediaTypes.has("video")) continue;
|
|
11767
|
+
if (track.streamType === "audio" && !mediaTypes.has("audio")) continue;
|
|
11768
|
+
const kind = track.streamType === "audio" ? "audio" : "video";
|
|
11769
|
+
try {
|
|
11770
|
+
const { bytes, mimeType } = await fetchMediaBytes(track.url, extraction.pageUrl);
|
|
11771
|
+
const stored = await store.put(`${prefix}/${trackFilename(track, index, extraction.shortcode)}`, bytes, mimeType ?? "video/mp4");
|
|
11772
|
+
downloads.push({ kind, url: track.url, savedPath: stored.url, sizeBytes: stored.bytes, mimeType, error: null });
|
|
11773
|
+
const isMuxInput = muxEligible && (track.url === extraction.selectedVideoTrack.url || track.url === extraction.selectedAudioTrack.url);
|
|
11774
|
+
if (isMuxInput) {
|
|
11775
|
+
if (!muxWorkDir) muxWorkDir = mkdtempSync(join4(tmpdir(), "ig-mux-"));
|
|
11776
|
+
const localPath = join4(muxWorkDir, trackFilename(track, index, extraction.shortcode));
|
|
11777
|
+
writeFileSync(localPath, bytes);
|
|
11778
|
+
muxInputs.set(track.url, localPath);
|
|
11779
|
+
}
|
|
11780
|
+
} catch (err) {
|
|
11781
|
+
downloads.push({ kind, url: track.url, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
|
|
11782
|
+
}
|
|
11741
11783
|
}
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11784
|
+
if (muxEligible && muxWorkDir && muxInputs.has(extraction.selectedVideoTrack.url) && muxInputs.has(extraction.selectedAudioTrack.url)) {
|
|
11785
|
+
const outPath = join4(muxWorkDir, "muxed.mp4");
|
|
11786
|
+
const muxed = await runFfmpegMux(muxInputs.get(extraction.selectedVideoTrack.url), muxInputs.get(extraction.selectedAudioTrack.url), outPath);
|
|
11787
|
+
if (muxed.ok) {
|
|
11788
|
+
const stored = await store.put(`${prefix}/muxed.mp4`, readFileSync2(outPath), "video/mp4");
|
|
11789
|
+
downloads.push({ kind: "muxed_video", url: null, savedPath: stored.url, sizeBytes: stored.bytes, mimeType: "video/mp4", error: null });
|
|
11790
|
+
} else {
|
|
11791
|
+
warnings.push(`Mux skipped/failed: ${muxed.error}`);
|
|
11792
|
+
}
|
|
11750
11793
|
}
|
|
11794
|
+
} catch (err) {
|
|
11795
|
+
warnings.push(`Media download unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
11796
|
+
} finally {
|
|
11797
|
+
if (muxWorkDir) rmSync(muxWorkDir, { recursive: true, force: true });
|
|
11751
11798
|
}
|
|
11752
11799
|
}
|
|
11753
11800
|
let transcript = null;
|
|
@@ -11770,7 +11817,7 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
|
|
|
11770
11817
|
const result = {
|
|
11771
11818
|
...extraction,
|
|
11772
11819
|
browser: launch.browser,
|
|
11773
|
-
outputDir,
|
|
11820
|
+
outputDir: null,
|
|
11774
11821
|
downloads,
|
|
11775
11822
|
warnings,
|
|
11776
11823
|
transcript
|
|
@@ -20079,7 +20126,7 @@ async function persistScrapeBody(user, opts) {
|
|
|
20079
20126
|
|
|
20080
20127
|
// src/api/scrape-blob-cleanup.ts
|
|
20081
20128
|
import { readdir, stat, unlink } from "fs/promises";
|
|
20082
|
-
import { homedir as
|
|
20129
|
+
import { homedir as homedir2 } from "os";
|
|
20083
20130
|
import { join as join6 } from "path";
|
|
20084
20131
|
async function cleanupVercel(token, cutoff) {
|
|
20085
20132
|
const { list, del } = await import("@vercel/blob");
|
|
@@ -20097,7 +20144,7 @@ async function cleanupVercel(token, cutoff) {
|
|
|
20097
20144
|
return { deleted, store: "vercel-blob" };
|
|
20098
20145
|
}
|
|
20099
20146
|
async function cleanupLocal(cutoff) {
|
|
20100
|
-
const baseDir = process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join6(
|
|
20147
|
+
const baseDir = process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join6(homedir2(), "Downloads", "mcp-scraper");
|
|
20101
20148
|
const dir = join6(baseDir, "blobs", SCRAPE_FALLBACK_PREFIX.replace(/\/$/, ""));
|
|
20102
20149
|
let deleted = 0;
|
|
20103
20150
|
let entries;
|
|
@@ -20453,6 +20500,7 @@ app.use("*", async (c, next) => {
|
|
|
20453
20500
|
});
|
|
20454
20501
|
function opForCostPath(p) {
|
|
20455
20502
|
if (p === "/harvest" || p === "/harvest/sync") return "harvest";
|
|
20503
|
+
if (p === "/diff-page") return "diff_page";
|
|
20456
20504
|
if (p === "/extract-url") return "page_scrape";
|
|
20457
20505
|
if (p === "/extract-site") return "extract_site";
|
|
20458
20506
|
if (p === "/map-urls") return "url_map";
|
|
@@ -20625,6 +20673,13 @@ app.get("/catalog", (c) => {
|
|
|
20625
20673
|
c.header("Cache-Control", "public, max-age=60");
|
|
20626
20674
|
return c.json(CATALOG);
|
|
20627
20675
|
});
|
|
20676
|
+
app.get("/rates", (c) => {
|
|
20677
|
+
const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => ({
|
|
20678
|
+
...cost,
|
|
20679
|
+
...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
|
|
20680
|
+
}));
|
|
20681
|
+
return c.json({ creditsPerDollarStarter: Math.round(1 / 15e-5), costs });
|
|
20682
|
+
});
|
|
20628
20683
|
app.get("/me", async (c) => {
|
|
20629
20684
|
const token = getCookie2(c, "session");
|
|
20630
20685
|
if (!token) return c.json({ authenticated: false });
|
|
@@ -21881,7 +21936,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
21881
21936
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
21882
21937
|
return c.json({ error: "Unauthorized" }, 401);
|
|
21883
21938
|
}
|
|
21884
|
-
const { drainQueue } = await import("./worker-
|
|
21939
|
+
const { drainQueue } = await import("./worker-JQTS437L.js");
|
|
21885
21940
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
21886
21941
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
21887
21942
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup] = await Promise.all([
|
|
@@ -21899,15 +21954,15 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
21899
21954
|
return c.json({ error: "Unauthorized" }, 401);
|
|
21900
21955
|
}
|
|
21901
21956
|
const jobId = c.req.param("id");
|
|
21902
|
-
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-
|
|
21903
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
21957
|
+
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-NVSZH35Y.js");
|
|
21958
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-U4D5LW5W.js");
|
|
21904
21959
|
const job = await getExtractJob2(jobId);
|
|
21905
21960
|
if (!job) return c.json({ error: "job not found" }, 404);
|
|
21906
21961
|
const stored = await assembleExtractArtifacts(job, {});
|
|
21907
21962
|
await completeExtractJob2(jobId, stored);
|
|
21908
21963
|
let settlement = "already_settled_or_refunded";
|
|
21909
21964
|
if (job.billedMc == null && job.userId != null) {
|
|
21910
|
-
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-
|
|
21965
|
+
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-NVSZH35Y.js");
|
|
21911
21966
|
const heldMc = Number(job.options.heldMc ?? 0);
|
|
21912
21967
|
const successful = await countSuccessfulPages2(jobId);
|
|
21913
21968
|
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
@@ -22071,4 +22126,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
22071
22126
|
export {
|
|
22072
22127
|
app
|
|
22073
22128
|
};
|
|
22074
|
-
//# sourceMappingURL=server-
|
|
22129
|
+
//# sourceMappingURL=server-WBDVVHR6.js.map
|