mcp-scraper 0.26.3 → 0.27.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/README.md +3 -3
- package/dist/bin/api-server.cjs +309 -270
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +3 -2
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +3 -3
- 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 +22 -9
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +3 -3
- package/dist/bin/paa-harvest.cjs +2 -1
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +2 -2
- package/dist/{chunk-FUWZWKGO.js → chunk-KAMKTSJQ.js} +2 -2
- package/dist/chunk-O6KFDDRM.js +7 -0
- package/dist/chunk-O6KFDDRM.js.map +1 -0
- package/dist/{chunk-73YM3DEB.js → chunk-RPR5LDLS.js} +22 -10
- package/dist/chunk-RPR5LDLS.js.map +1 -0
- package/dist/{chunk-E5J4HJBO.js → chunk-YH5DRFWQ.js} +4 -2
- package/dist/{chunk-XGIPATLV.js → chunk-ZQSHKWV4.js} +3 -2
- package/dist/chunk-ZQSHKWV4.js.map +1 -0
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{server-GN34MGAE.js → server-YXVSZ3T7.js} +287 -261
- package/dist/{server-GN34MGAE.js.map → server-YXVSZ3T7.js.map} +1 -1
- package/dist/{worker-E3KHQH2F.js → worker-RYP5EOV5.js} +3 -3
- package/docs/mcp-tool-manifest.generated.json +37 -8
- package/package.json +1 -1
- package/dist/chunk-27VXTOD2.js +0 -7
- package/dist/chunk-27VXTOD2.js.map +0 -1
- package/dist/chunk-73YM3DEB.js.map +0 -1
- package/dist/chunk-XGIPATLV.js.map +0 -1
- /package/dist/{chunk-FUWZWKGO.js.map → chunk-KAMKTSJQ.js.map} +0 -0
- /package/dist/{chunk-E5J4HJBO.js.map → chunk-YH5DRFWQ.js.map} +0 -0
- /package/dist/{worker-E3KHQH2F.js.map → worker-RYP5EOV5.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
harvest
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YH5DRFWQ.js";
|
|
4
4
|
import {
|
|
5
5
|
recordVendorUsage
|
|
6
6
|
} from "./chunk-MTSBI7ZH.js";
|
|
7
7
|
import "./chunk-K443GQY5.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-ZQSHKWV4.js";
|
|
9
9
|
import "./chunk-M2S27J6Z.js";
|
|
10
10
|
import "./chunk-ICT7DDHL.js";
|
|
11
11
|
|
|
@@ -12,9 +12,11 @@ import {
|
|
|
12
12
|
MapsSelectors,
|
|
13
13
|
buildYouTubeChannelVideosUrl,
|
|
14
14
|
deleteKernelProxyId,
|
|
15
|
+
encodeUule,
|
|
15
16
|
harvest,
|
|
17
|
+
normalizeLocation,
|
|
16
18
|
resolveKernelProxyId
|
|
17
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-YH5DRFWQ.js";
|
|
18
20
|
import {
|
|
19
21
|
HttpMcpToolExecutor,
|
|
20
22
|
assessTranscriptSignal,
|
|
@@ -34,7 +36,7 @@ import {
|
|
|
34
36
|
sanitizeAttempts,
|
|
35
37
|
sanitizeHarvestResult,
|
|
36
38
|
transcribeMediaUrl
|
|
37
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-RPR5LDLS.js";
|
|
38
40
|
import {
|
|
39
41
|
auditImages,
|
|
40
42
|
buildLinkReport,
|
|
@@ -63,7 +65,7 @@ import {
|
|
|
63
65
|
workflowOutputBaseDir,
|
|
64
66
|
workflowStepCount,
|
|
65
67
|
workflowSupportsSteps
|
|
66
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-KAMKTSJQ.js";
|
|
67
69
|
import "./chunk-K443GQY5.js";
|
|
68
70
|
import {
|
|
69
71
|
DEFAULT_MAPS_PROXY_MODE,
|
|
@@ -74,8 +76,8 @@ import {
|
|
|
74
76
|
RawMapsHoursRowSchema,
|
|
75
77
|
RawMapsOverviewSchema,
|
|
76
78
|
RawMapsReviewStatsSchema
|
|
77
|
-
} from "./chunk-
|
|
78
|
-
import "./chunk-
|
|
79
|
+
} from "./chunk-ZQSHKWV4.js";
|
|
80
|
+
import "./chunk-O6KFDDRM.js";
|
|
79
81
|
import {
|
|
80
82
|
completeExtractJob,
|
|
81
83
|
countSuccessfulPages,
|
|
@@ -13077,6 +13079,262 @@ videoApp.post("/status", createApiKeyAuth(), async (c) => {
|
|
|
13077
13079
|
// src/api/maps-routes.ts
|
|
13078
13080
|
import { Hono as Hono11 } from "hono";
|
|
13079
13081
|
|
|
13082
|
+
// src/extractor/MapsSearchExtractor.ts
|
|
13083
|
+
var LOCAL_RESULTS_PAGE_LIMIT = 8;
|
|
13084
|
+
var LOCAL_RESULTS_WAIT_MS = 1e3;
|
|
13085
|
+
function buildGoogleLocalResultsUrl(input) {
|
|
13086
|
+
const query = [input.query, input.location].filter(Boolean).join(" ");
|
|
13087
|
+
const params = new URLSearchParams({ q: query, udm: "1", gl: input.gl, hl: input.hl, pws: "0" });
|
|
13088
|
+
if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
|
|
13089
|
+
return `https://www.google.com/search?${params.toString()}`;
|
|
13090
|
+
}
|
|
13091
|
+
var MapsSearchExtractor = class {
|
|
13092
|
+
constructor(driver) {
|
|
13093
|
+
this.driver = driver;
|
|
13094
|
+
}
|
|
13095
|
+
driver;
|
|
13096
|
+
async extract(options) {
|
|
13097
|
+
const startMs = Date.now();
|
|
13098
|
+
const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
|
|
13099
|
+
const searchUrl = buildGoogleLocalResultsUrl(options);
|
|
13100
|
+
const config = {
|
|
13101
|
+
headless: options.headless,
|
|
13102
|
+
kernelApiKey: options.kernelApiKey,
|
|
13103
|
+
kernelProxyId: options.kernelProxyId,
|
|
13104
|
+
kernelProxyResolution: options.kernelProxyResolution,
|
|
13105
|
+
proxyMode: options.proxyMode,
|
|
13106
|
+
viewport: { width: 1280, height: 900 },
|
|
13107
|
+
locale: `${options.hl}-${options.gl.toUpperCase()}`,
|
|
13108
|
+
debug: options.debug
|
|
13109
|
+
};
|
|
13110
|
+
try {
|
|
13111
|
+
await this.driver.launch(config);
|
|
13112
|
+
const page = this.driver.getPage();
|
|
13113
|
+
await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
13114
|
+
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
13115
|
+
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
13116
|
+
const results = await this.collectResults(page, options);
|
|
13117
|
+
return {
|
|
13118
|
+
query: options.query,
|
|
13119
|
+
location: options.location ?? null,
|
|
13120
|
+
searchQuery,
|
|
13121
|
+
searchUrl,
|
|
13122
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13123
|
+
requestedMaxResults: options.maxResults,
|
|
13124
|
+
resultCount: results.length,
|
|
13125
|
+
results,
|
|
13126
|
+
durationMs: Date.now() - startMs
|
|
13127
|
+
};
|
|
13128
|
+
} finally {
|
|
13129
|
+
await this.driver.close();
|
|
13130
|
+
}
|
|
13131
|
+
}
|
|
13132
|
+
async detectBlock(page) {
|
|
13133
|
+
return page.evaluate(() => {
|
|
13134
|
+
const text2 = document.body.innerText.slice(0, 2e3);
|
|
13135
|
+
return /unusual traffic|captcha|recaptcha|about this page/i.test(text2) || /\/sorry\//.test(location.href);
|
|
13136
|
+
});
|
|
13137
|
+
}
|
|
13138
|
+
async collectResults(page, options) {
|
|
13139
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13140
|
+
const results = [];
|
|
13141
|
+
let pageCount = 0;
|
|
13142
|
+
while (results.length < options.maxResults && pageCount < LOCAL_RESULTS_PAGE_LIMIT) {
|
|
13143
|
+
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
13144
|
+
const cards = await this.extractVisibleCards(page, options.location);
|
|
13145
|
+
for (const card of cards) {
|
|
13146
|
+
if (results.length >= options.maxResults || seen.has(card.cardKey)) continue;
|
|
13147
|
+
seen.add(card.cardKey);
|
|
13148
|
+
const details = await this.openCardAndExtractDialog(page, card, options.includeServices);
|
|
13149
|
+
results.push({
|
|
13150
|
+
position: results.length + 1,
|
|
13151
|
+
name: card.name,
|
|
13152
|
+
placeUrl: details?.placeUrl ?? card.placeUrl,
|
|
13153
|
+
cid: details?.cid ?? card.cid,
|
|
13154
|
+
cidDecimal: details?.cidDecimal ?? card.cidDecimal,
|
|
13155
|
+
rating: details?.rating ?? card.rating,
|
|
13156
|
+
reviewCount: details?.reviewCount ?? card.reviewCount,
|
|
13157
|
+
category: details?.category ?? card.category,
|
|
13158
|
+
address: details?.address ?? card.address,
|
|
13159
|
+
phone: details?.phone ?? card.phone,
|
|
13160
|
+
hoursStatus: details?.hoursStatus ?? card.hoursStatus,
|
|
13161
|
+
websiteUrl: details?.websiteUrl ?? card.websiteUrl,
|
|
13162
|
+
directionsUrl: card.directionsUrl,
|
|
13163
|
+
metadata: card.metadata,
|
|
13164
|
+
services: details?.services ?? [],
|
|
13165
|
+
areasServed: details?.areasServed ?? [],
|
|
13166
|
+
profileDetailsStatus: details ? "collected" : "unavailable"
|
|
13167
|
+
});
|
|
13168
|
+
}
|
|
13169
|
+
if (results.length >= options.maxResults) break;
|
|
13170
|
+
const advanced = await this.advanceToMoreResults(page);
|
|
13171
|
+
if (!advanced) break;
|
|
13172
|
+
pageCount += 1;
|
|
13173
|
+
}
|
|
13174
|
+
return results;
|
|
13175
|
+
}
|
|
13176
|
+
async extractVisibleCards(page, location2) {
|
|
13177
|
+
const fallbackLocation = location2 ?? "";
|
|
13178
|
+
return page.evaluate((locationHint) => {
|
|
13179
|
+
const normalize = (value) => {
|
|
13180
|
+
const text2 = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
13181
|
+
return text2 || null;
|
|
13182
|
+
};
|
|
13183
|
+
const phonePattern = /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/;
|
|
13184
|
+
const addressPattern = /\b[A-Z]{2}\s+\d{5}(?:-\d{4})?\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Boulevard|Dr|Drive|Ln|Lane|Way|Pkwy|Parkway)\b/i;
|
|
13185
|
+
const cards = Array.from(document.querySelectorAll("button")).filter((button) => {
|
|
13186
|
+
const name = normalize(button.querySelector("h3")?.textContent);
|
|
13187
|
+
return Boolean(name) && !button.closest('[role="dialog"], dialog') && !/\bsponsored\b/i.test(button.innerText);
|
|
13188
|
+
});
|
|
13189
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13190
|
+
const out = [];
|
|
13191
|
+
for (const [cardIndex, card] of cards.entries()) {
|
|
13192
|
+
const name = normalize(card.querySelector("h3")?.textContent);
|
|
13193
|
+
if (!name) continue;
|
|
13194
|
+
const text2 = normalize(card.innerText) ?? name;
|
|
13195
|
+
const key = `${name.toLowerCase()}|${text2.toLowerCase()}`;
|
|
13196
|
+
if (seen.has(key)) continue;
|
|
13197
|
+
seen.add(key);
|
|
13198
|
+
const lines = text2.split(/\n+/).map((line) => normalize(line)).filter((line) => Boolean(line));
|
|
13199
|
+
const aria = normalize(card.getAttribute("aria-label"));
|
|
13200
|
+
const rating = (aria ?? text2).match(/(?:rated\s+)?(\d(?:\.\d)?)\s*(?:out of 5)?/i)?.[1] ?? null;
|
|
13201
|
+
const reviewCount = (aria ?? text2).match(/(?:\(|·\s*)([\d,]+)\s+(?:user\s+)?reviews?\b/i)?.[1]?.replace(/,/g, "") ?? null;
|
|
13202
|
+
const phone = lines.find((line) => phonePattern.test(line)) ?? null;
|
|
13203
|
+
const address = lines.find((line) => addressPattern.test(line)) ?? null;
|
|
13204
|
+
const hoursStatus = lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? null;
|
|
13205
|
+
const category = lines.find(
|
|
13206
|
+
(line) => line !== name && line !== phone && line !== address && line !== hoursStatus && !/^(?:rated|\d[\d.,]*\s*(?:out of 5|reviews?)?)\b/i.test(line) && !/sponsored|website|directions|call|save|share|more/i.test(line)
|
|
13207
|
+
) ?? null;
|
|
13208
|
+
const mapsSearchUrl = `https://www.google.com/maps/search/${encodeURIComponent([name, locationHint].filter(Boolean).join(" "))}`;
|
|
13209
|
+
out.push({
|
|
13210
|
+
cardIndex,
|
|
13211
|
+
cardKey: key,
|
|
13212
|
+
name,
|
|
13213
|
+
placeUrl: mapsSearchUrl,
|
|
13214
|
+
cid: null,
|
|
13215
|
+
cidDecimal: null,
|
|
13216
|
+
rating,
|
|
13217
|
+
reviewCount,
|
|
13218
|
+
category,
|
|
13219
|
+
address,
|
|
13220
|
+
phone,
|
|
13221
|
+
hoursStatus,
|
|
13222
|
+
websiteUrl: null,
|
|
13223
|
+
directionsUrl: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
|
|
13224
|
+
metadata: lines.slice(0, 20)
|
|
13225
|
+
});
|
|
13226
|
+
}
|
|
13227
|
+
return out;
|
|
13228
|
+
}, fallbackLocation);
|
|
13229
|
+
}
|
|
13230
|
+
async openCardAndExtractDialog(page, card, includeServices) {
|
|
13231
|
+
const clicked = await page.evaluate((input) => {
|
|
13232
|
+
const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
13233
|
+
const cards = Array.from(document.querySelectorAll("button")).filter(
|
|
13234
|
+
(button) => Boolean(button.querySelector("h3")) && !button.closest('[role="dialog"], dialog') && !/\bsponsored\b/i.test(button.innerText)
|
|
13235
|
+
);
|
|
13236
|
+
const exact = cards.find((button) => `${normalize(button.querySelector("h3")?.textContent)}|${normalize(button.innerText)}` === input.cardKey);
|
|
13237
|
+
const target = exact ?? cards[input.cardIndex];
|
|
13238
|
+
if (!target) return false;
|
|
13239
|
+
target.click();
|
|
13240
|
+
return true;
|
|
13241
|
+
}, { cardIndex: card.cardIndex, cardKey: card.cardKey });
|
|
13242
|
+
if (!clicked) return null;
|
|
13243
|
+
const dialog = page.locator('dialog, [role="dialog"]').filter({ has: page.locator('h1, h2, [role="heading"]') }).last();
|
|
13244
|
+
await dialog.waitFor({ state: "visible", timeout: 6e3 }).catch(() => {
|
|
13245
|
+
});
|
|
13246
|
+
if (await dialog.count() === 0 || !await dialog.isVisible().catch(() => false)) return null;
|
|
13247
|
+
const details = await dialog.evaluate((root, includeServicesValue) => {
|
|
13248
|
+
const text2 = (value) => value?.replace(/\s+/g, " ").trim() ?? "";
|
|
13249
|
+
const phonePattern = /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/;
|
|
13250
|
+
const buttons = Array.from(root.querySelectorAll("button"));
|
|
13251
|
+
const buttonTexts = buttons.map((button) => text2(button.innerText)).filter(Boolean);
|
|
13252
|
+
const servicesText = includeServicesValue ? buttonTexts.find((value) => /^services\s*:/i.test(value)) : null;
|
|
13253
|
+
const services = servicesText ? [...new Set(servicesText.replace(/^services\s*:\s*/i, "").split(/\s*[,·]\s*/).map((value) => value.trim()).filter(Boolean))] : [];
|
|
13254
|
+
const areaText = includeServicesValue ? buttonTexts.find((value) => /^serves\b/i.test(value)) : null;
|
|
13255
|
+
const areasServed = areaText ? [areaText.replace(/^serves\s*/i, "").trim()].filter(Boolean) : [];
|
|
13256
|
+
const externalLinks = Array.from(root.querySelectorAll("a[href]"));
|
|
13257
|
+
const website = externalLinks.find((link) => /^website$/i.test(text2(link.innerText)) || /^website$/i.test(link.getAttribute("aria-label") ?? ""));
|
|
13258
|
+
const mapLink = externalLinks.find((link) => /google\.[^/]+\/maps|\/maps\//i.test(link.href));
|
|
13259
|
+
const addressLink = externalLinks.find((link) => /\b[A-Z]{2}\s+\d{5}(?:-\d{4})?\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Boulevard|Dr|Drive|Ln|Lane|Way|Pkwy|Parkway)\b/i.test(text2(link.innerText)));
|
|
13260
|
+
const allText = text2(root.innerText);
|
|
13261
|
+
const rating = allText.match(/\b(?:rated\s+)?(\d(?:\.\d)?)\s+out of 5\b/i)?.[1] ?? allText.match(/\b(\d(?:\.\d)?)\s+rated\b/i)?.[1] ?? null;
|
|
13262
|
+
const reviewCount = allText.match(/([\d,]+)\s+(?:user\s+)?reviews?\b/i)?.[1]?.replace(/,/g, "") ?? null;
|
|
13263
|
+
const phoneButton = buttonTexts.find((value) => /^call\b/i.test(value) || phonePattern.test(value)) ?? "";
|
|
13264
|
+
const phone = phoneButton.match(phonePattern)?.[0] ?? null;
|
|
13265
|
+
const hoursStatus = buttonTexts.find((value) => /\b(?:open|closed|opens|closes)\b/i.test(value)) ?? null;
|
|
13266
|
+
const placeUrl = mapLink?.href ?? addressLink?.href ?? location.href;
|
|
13267
|
+
const fid = placeUrl.match(/!1s(0x[0-9a-f]+):(0x[0-9a-f]+)/i);
|
|
13268
|
+
const cid = fid ? `${fid[1]}:${fid[2]}` : null;
|
|
13269
|
+
let cidDecimal = null;
|
|
13270
|
+
if (fid) {
|
|
13271
|
+
try {
|
|
13272
|
+
cidDecimal = BigInt(fid[2]).toString();
|
|
13273
|
+
} catch {
|
|
13274
|
+
}
|
|
13275
|
+
}
|
|
13276
|
+
if (!cidDecimal) {
|
|
13277
|
+
const cidFromQuery = new URL(placeUrl).searchParams.get("cid");
|
|
13278
|
+
cidDecimal = cidFromQuery && /^\d+$/.test(cidFromQuery) ? cidFromQuery : null;
|
|
13279
|
+
}
|
|
13280
|
+
return {
|
|
13281
|
+
placeUrl,
|
|
13282
|
+
cid,
|
|
13283
|
+
cidDecimal,
|
|
13284
|
+
rating,
|
|
13285
|
+
reviewCount,
|
|
13286
|
+
category: null,
|
|
13287
|
+
address: text2(addressLink?.innerText) || null,
|
|
13288
|
+
phone,
|
|
13289
|
+
websiteUrl: website?.href ?? null,
|
|
13290
|
+
hoursStatus,
|
|
13291
|
+
services,
|
|
13292
|
+
areasServed
|
|
13293
|
+
};
|
|
13294
|
+
}, includeServices);
|
|
13295
|
+
await this.closeDialog(page);
|
|
13296
|
+
return details;
|
|
13297
|
+
}
|
|
13298
|
+
async closeDialog(page) {
|
|
13299
|
+
const closed = await page.evaluate(() => {
|
|
13300
|
+
const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]'));
|
|
13301
|
+
const dialog = dialogs.find((candidate) => candidate.offsetParent !== null);
|
|
13302
|
+
const close = dialog && Array.from(dialog.querySelectorAll("button")).find(
|
|
13303
|
+
(button) => /^close$/i.test(button.getAttribute("aria-label") ?? "") || /^close$/i.test(button.innerText.trim())
|
|
13304
|
+
);
|
|
13305
|
+
close?.click();
|
|
13306
|
+
return Boolean(close);
|
|
13307
|
+
});
|
|
13308
|
+
if (closed) await page.waitForTimeout(250);
|
|
13309
|
+
}
|
|
13310
|
+
async advanceToMoreResults(page) {
|
|
13311
|
+
const before = await page.evaluate(() => document.body.innerText.slice(-2e3));
|
|
13312
|
+
const advanced = await page.evaluate(() => {
|
|
13313
|
+
const visible = (element) => element.offsetParent !== null;
|
|
13314
|
+
const mobile = Array.from(document.querySelectorAll("button, a")).find(
|
|
13315
|
+
(element) => visible(element) && /^(more search results|more results)$/i.test(element.innerText.trim())
|
|
13316
|
+
);
|
|
13317
|
+
if (mobile) {
|
|
13318
|
+
mobile.click();
|
|
13319
|
+
return true;
|
|
13320
|
+
}
|
|
13321
|
+
const next = Array.from(document.querySelectorAll("a[href]")).find(
|
|
13322
|
+
(link) => visible(link) && (/^next$/i.test(link.innerText.trim()) || /[?&]start=\d+/.test(link.href))
|
|
13323
|
+
);
|
|
13324
|
+
if (next) {
|
|
13325
|
+
next.click();
|
|
13326
|
+
return true;
|
|
13327
|
+
}
|
|
13328
|
+
return false;
|
|
13329
|
+
});
|
|
13330
|
+
if (!advanced) return false;
|
|
13331
|
+
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
13332
|
+
await page.waitForFunction((previous) => document.body.innerText.slice(-2e3) !== previous, before, { timeout: 5e3 }).catch(() => {
|
|
13333
|
+
});
|
|
13334
|
+
return true;
|
|
13335
|
+
}
|
|
13336
|
+
};
|
|
13337
|
+
|
|
13080
13338
|
// src/extractor/MapsNavigator.ts
|
|
13081
13339
|
var MapsNavigator = class {
|
|
13082
13340
|
constructor(page) {
|
|
@@ -13135,32 +13393,26 @@ var MapsNavigator = class {
|
|
|
13135
13393
|
await this.page.waitForSelector('[role="main"] h2', { timeout: 5e3 }).catch(() => {
|
|
13136
13394
|
});
|
|
13137
13395
|
}
|
|
13138
|
-
async navigateToSerpBusinessPanel(businessName, location2) {
|
|
13139
|
-
const
|
|
13140
|
-
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}&udm=1`;
|
|
13396
|
+
async navigateToSerpBusinessPanel(businessName, location2, gl, hl) {
|
|
13397
|
+
const searchUrl = buildGoogleLocalResultsUrl({ query: businessName, location: location2, gl, hl });
|
|
13141
13398
|
await this.page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch(() => {
|
|
13142
13399
|
});
|
|
13143
|
-
await this.page.waitForTimeout(
|
|
13400
|
+
await this.page.waitForTimeout(1e3);
|
|
13144
13401
|
const clicked = await this.page.evaluate((name) => {
|
|
13145
|
-
const
|
|
13146
|
-
const
|
|
13147
|
-
|
|
13148
|
-
|
|
13149
|
-
|
|
13150
|
-
|
|
13151
|
-
if (clickable) {
|
|
13152
|
-
clickable.click();
|
|
13153
|
-
return true;
|
|
13154
|
-
}
|
|
13155
|
-
if (heading) {
|
|
13156
|
-
heading.click();
|
|
13402
|
+
const normalized = name.replace(/\s+/g, " ").trim().toLowerCase();
|
|
13403
|
+
const card = Array.from(document.querySelectorAll("button")).find(
|
|
13404
|
+
(button) => button.querySelector("h3")?.textContent?.replace(/\s+/g, " ").trim().toLowerCase() === normalized
|
|
13405
|
+
);
|
|
13406
|
+
if (card) {
|
|
13407
|
+
card.click();
|
|
13157
13408
|
return true;
|
|
13158
13409
|
}
|
|
13159
13410
|
return false;
|
|
13160
13411
|
}, businessName);
|
|
13161
13412
|
if (!clicked) return false;
|
|
13162
|
-
await this.page.
|
|
13163
|
-
|
|
13413
|
+
await this.page.waitForSelector('dialog, [role="dialog"]', { timeout: 5e3 }).catch(() => {
|
|
13414
|
+
});
|
|
13415
|
+
return this.page.locator('dialog, [role="dialog"]').count().then((count) => count > 0);
|
|
13164
13416
|
}
|
|
13165
13417
|
buildReviewsUrl(placeUrl) {
|
|
13166
13418
|
const fid = placeUrl.match(/!1s(0x[0-9a-f]+:0x[0-9a-f]+)/i);
|
|
@@ -13336,7 +13588,7 @@ var MapsExtractor = class {
|
|
|
13336
13588
|
let servicesStatus = "not_requested";
|
|
13337
13589
|
if (options.includeServices) {
|
|
13338
13590
|
try {
|
|
13339
|
-
const opened = await navigator.navigateToSerpBusinessPanel(options.businessName, options.location);
|
|
13591
|
+
const opened = await navigator.navigateToSerpBusinessPanel(options.businessName, options.location, options.gl, options.hl);
|
|
13340
13592
|
if (opened) {
|
|
13341
13593
|
const result = await this.extractServicesAndAreasServed(page);
|
|
13342
13594
|
services = result.services;
|
|
@@ -13557,242 +13809,15 @@ var MapsExtractor = class {
|
|
|
13557
13809
|
return result.data;
|
|
13558
13810
|
}
|
|
13559
13811
|
async extractServicesAndAreasServed(page) {
|
|
13560
|
-
await page.waitForTimeout(1500);
|
|
13561
|
-
let services = [];
|
|
13562
|
-
if (await this.clickRowStartingWithRetry(page, "Services:")) {
|
|
13563
|
-
await page.waitForSelector('[role="dialog"]', { timeout: 5e3 }).catch(() => {
|
|
13564
|
-
});
|
|
13565
|
-
await page.waitForTimeout(800);
|
|
13566
|
-
services = await this.extractOpenDialogItems(page, "Services");
|
|
13567
|
-
await this.closeOpenDialog(page, "Services");
|
|
13568
|
-
await page.waitForTimeout(500);
|
|
13569
|
-
}
|
|
13570
|
-
let areasServed = [];
|
|
13571
|
-
if (await this.clickRowStartingWithRetry(page, "Serves ")) {
|
|
13572
|
-
await page.waitForSelector('[role="dialog"]', { timeout: 5e3 }).catch(() => {
|
|
13573
|
-
});
|
|
13574
|
-
await page.waitForTimeout(800);
|
|
13575
|
-
areasServed = await this.extractOpenDialogItems(page, "Areas Served");
|
|
13576
|
-
await this.closeOpenDialog(page, "Areas Served");
|
|
13577
|
-
}
|
|
13578
|
-
return { services, areasServed };
|
|
13579
|
-
}
|
|
13580
|
-
async clickRowStartingWithRetry(page, prefix) {
|
|
13581
|
-
if (await this.clickRowStartingWith(page, prefix)) return true;
|
|
13582
|
-
await page.waitForTimeout(2e3);
|
|
13583
|
-
return this.clickRowStartingWith(page, prefix);
|
|
13584
|
-
}
|
|
13585
|
-
async clickRowStartingWith(page, prefix) {
|
|
13586
|
-
return page.evaluate((p) => {
|
|
13587
|
-
const el2 = Array.from(document.querySelectorAll('button, a, div[role="button"]')).find(
|
|
13588
|
-
(e) => (e.innerText ?? "").trim().startsWith(p)
|
|
13589
|
-
);
|
|
13590
|
-
if (!el2) return false;
|
|
13591
|
-
el2.click();
|
|
13592
|
-
return true;
|
|
13593
|
-
}, prefix);
|
|
13594
|
-
}
|
|
13595
|
-
async extractOpenDialogItems(page, expectedHeading) {
|
|
13596
|
-
return page.evaluate((expected) => {
|
|
13597
|
-
const dialogs = Array.from(document.querySelectorAll('[role="dialog"]'));
|
|
13598
|
-
const withHeading = dialogs.find((d) => {
|
|
13599
|
-
const h = d.querySelector('h1, h2, [role="heading"]');
|
|
13600
|
-
const headingText = h?.innerText?.trim() ?? "";
|
|
13601
|
-
return headingText.startsWith(expected);
|
|
13602
|
-
});
|
|
13603
|
-
if (!withHeading) return [];
|
|
13604
|
-
const headingEl = withHeading.querySelector('h1, h2, [role="heading"]');
|
|
13605
|
-
const headingLines = new Set(
|
|
13606
|
-
(headingEl?.innerText ?? "").split("\n").map((s) => s.trim()).filter(Boolean)
|
|
13607
|
-
);
|
|
13608
|
-
const seen = /* @__PURE__ */ new Set();
|
|
13609
|
-
const items = [];
|
|
13610
|
-
withHeading.querySelectorAll("*").forEach((el2) => {
|
|
13611
|
-
if (el2.children.length > 0) return;
|
|
13612
|
-
const text2 = el2.innerText?.trim() ?? el2.textContent?.trim() ?? "";
|
|
13613
|
-
if (!text2 || headingLines.has(text2)) return;
|
|
13614
|
-
if (/^(back|close|done|thanks)$/i.test(text2)) return;
|
|
13615
|
-
if (/^serves\s.*following/i.test(text2)) return;
|
|
13616
|
-
if (seen.has(text2)) return;
|
|
13617
|
-
seen.add(text2);
|
|
13618
|
-
items.push(text2);
|
|
13619
|
-
});
|
|
13620
|
-
return items;
|
|
13621
|
-
}, expectedHeading);
|
|
13622
|
-
}
|
|
13623
|
-
async closeOpenDialog(page, expectedHeading) {
|
|
13624
|
-
await page.evaluate((expected) => {
|
|
13625
|
-
const dialog = Array.from(document.querySelectorAll('[role="dialog"]')).find((d) => {
|
|
13626
|
-
const h = d.querySelector('h1, h2, [role="heading"]');
|
|
13627
|
-
const headingText = h?.innerText?.trim() ?? "";
|
|
13628
|
-
return headingText.startsWith(expected);
|
|
13629
|
-
});
|
|
13630
|
-
const closeBtn = dialog?.querySelector('[aria-label*="Close" i], button[aria-label*="close" i]');
|
|
13631
|
-
closeBtn?.click();
|
|
13632
|
-
}, expectedHeading);
|
|
13633
|
-
}
|
|
13634
|
-
};
|
|
13635
|
-
|
|
13636
|
-
// src/extractor/MapsSearchExtractor.ts
|
|
13637
|
-
var MAPS_SEARCH_SCROLL_BUDGET_MS = 6e4;
|
|
13638
|
-
var MAPS_SEARCH_SCROLL_STEP_MS = 1200;
|
|
13639
|
-
var MAPS_SEARCH_MAX_NO_GROWTH_ROUNDS = 4;
|
|
13640
|
-
var MapsSearchExtractor = class {
|
|
13641
|
-
constructor(driver) {
|
|
13642
|
-
this.driver = driver;
|
|
13643
|
-
}
|
|
13644
|
-
driver;
|
|
13645
|
-
async extract(options) {
|
|
13646
|
-
const startMs = Date.now();
|
|
13647
|
-
const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
|
|
13648
|
-
const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(searchQuery)}?hl=${encodeURIComponent(options.hl)}`;
|
|
13649
|
-
const config = {
|
|
13650
|
-
headless: options.headless,
|
|
13651
|
-
kernelApiKey: options.kernelApiKey,
|
|
13652
|
-
kernelProxyId: options.kernelProxyId,
|
|
13653
|
-
kernelProxyResolution: options.kernelProxyResolution,
|
|
13654
|
-
proxyMode: options.proxyMode,
|
|
13655
|
-
viewport: { width: 1280, height: 900 },
|
|
13656
|
-
locale: `${options.hl}-${options.gl.toUpperCase()}`,
|
|
13657
|
-
debug: options.debug
|
|
13658
|
-
};
|
|
13659
|
-
try {
|
|
13660
|
-
await this.driver.launch(config);
|
|
13661
|
-
const page = this.driver.getPage();
|
|
13662
|
-
await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
13663
|
-
await page.waitForTimeout(3e3);
|
|
13664
|
-
const blocked = await this.detectBlock(page);
|
|
13665
|
-
if (blocked) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
13666
|
-
const results = await this.collectResults(page, options.maxResults);
|
|
13667
|
-
return {
|
|
13668
|
-
query: options.query,
|
|
13669
|
-
location: options.location ?? null,
|
|
13670
|
-
searchQuery,
|
|
13671
|
-
searchUrl,
|
|
13672
|
-
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13673
|
-
requestedMaxResults: options.maxResults,
|
|
13674
|
-
resultCount: results.length,
|
|
13675
|
-
results,
|
|
13676
|
-
durationMs: Date.now() - startMs
|
|
13677
|
-
};
|
|
13678
|
-
} finally {
|
|
13679
|
-
await this.driver.close();
|
|
13680
|
-
}
|
|
13681
|
-
}
|
|
13682
|
-
async detectBlock(page) {
|
|
13683
13812
|
return page.evaluate(() => {
|
|
13684
|
-
const
|
|
13685
|
-
return
|
|
13686
|
-
|
|
13687
|
-
|
|
13688
|
-
|
|
13689
|
-
|
|
13690
|
-
|
|
13691
|
-
|
|
13692
|
-
while (Date.now() - started < MAPS_SEARCH_SCROLL_BUDGET_MS) {
|
|
13693
|
-
const before = seen.size;
|
|
13694
|
-
const batch = await this.extractVisibleResults(page);
|
|
13695
|
-
for (const result of batch) {
|
|
13696
|
-
const key = this.resultKey(result);
|
|
13697
|
-
if (!seen.has(key)) seen.set(key, { ...result, position: seen.size + 1 });
|
|
13698
|
-
if (seen.size >= maxResults) break;
|
|
13699
|
-
}
|
|
13700
|
-
if (seen.size >= maxResults) break;
|
|
13701
|
-
if (seen.size === before) noGrowthRounds += 1;
|
|
13702
|
-
else noGrowthRounds = 0;
|
|
13703
|
-
if (noGrowthRounds >= MAPS_SEARCH_MAX_NO_GROWTH_ROUNDS) break;
|
|
13704
|
-
await page.evaluate(() => {
|
|
13705
|
-
const feed = document.querySelector('[role="feed"]');
|
|
13706
|
-
if (feed) {
|
|
13707
|
-
feed.scrollTop = feed.scrollHeight;
|
|
13708
|
-
} else {
|
|
13709
|
-
window.scrollTo(0, document.body.scrollHeight);
|
|
13710
|
-
}
|
|
13711
|
-
});
|
|
13712
|
-
await page.waitForTimeout(MAPS_SEARCH_SCROLL_STEP_MS);
|
|
13713
|
-
}
|
|
13714
|
-
return [...seen.values()].slice(0, maxResults);
|
|
13715
|
-
}
|
|
13716
|
-
resultKey(result) {
|
|
13717
|
-
return result.cidDecimal ?? result.placeUrl.replace(/[?&].*$/, "") ?? result.name;
|
|
13718
|
-
}
|
|
13719
|
-
async extractVisibleResults(page) {
|
|
13720
|
-
return page.evaluate(() => {
|
|
13721
|
-
function normalizeText(value) {
|
|
13722
|
-
const text2 = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
13723
|
-
return text2 || null;
|
|
13724
|
-
}
|
|
13725
|
-
function cidFromUrl(url) {
|
|
13726
|
-
const fid = url.match(/!1s(0x[0-9a-f]+):(0x[0-9a-f]+)/i);
|
|
13727
|
-
if (!fid) return { cid: null, cidDecimal: null };
|
|
13728
|
-
let cidDecimal = null;
|
|
13729
|
-
try {
|
|
13730
|
-
cidDecimal = BigInt(fid[2]).toString();
|
|
13731
|
-
} catch {
|
|
13732
|
-
}
|
|
13733
|
-
return { cid: `${fid[1]}:${fid[2]}`, cidDecimal };
|
|
13734
|
-
}
|
|
13735
|
-
function textParts(card) {
|
|
13736
|
-
if (!card) return [];
|
|
13737
|
-
const parts = [];
|
|
13738
|
-
card.querySelectorAll("div, span").forEach((el2) => {
|
|
13739
|
-
const text2 = Array.from(el2.childNodes).filter((node) => node.nodeType === 3).map((node) => node.textContent?.trim() ?? "").filter((text3) => text3.length > 1 && text3.length < 140).join(" ");
|
|
13740
|
-
if (text2 && !parts.includes(text2)) parts.push(text2);
|
|
13741
|
-
});
|
|
13742
|
-
return parts;
|
|
13743
|
-
}
|
|
13744
|
-
function firstMatching(parts, pattern) {
|
|
13745
|
-
const value = parts.find((part) => pattern.test(part));
|
|
13746
|
-
return value ?? null;
|
|
13747
|
-
}
|
|
13748
|
-
function normalizedSet(values) {
|
|
13749
|
-
return new Set(values.filter(Boolean).map((value) => value.toLowerCase()));
|
|
13750
|
-
}
|
|
13751
|
-
const out = [];
|
|
13752
|
-
const seen = /* @__PURE__ */ new Set();
|
|
13753
|
-
const anchors = Array.from(document.querySelectorAll('a[href*="/maps/place/"]'));
|
|
13754
|
-
for (const anchor of anchors) {
|
|
13755
|
-
const placeUrl = anchor.href;
|
|
13756
|
-
const stableUrl = placeUrl.replace(/[?&].*$/, "");
|
|
13757
|
-
if (seen.has(stableUrl)) continue;
|
|
13758
|
-
seen.add(stableUrl);
|
|
13759
|
-
const card = anchor.closest('.Nv2PK, [role="article"], .bfdHYd') ?? anchor.parentElement;
|
|
13760
|
-
const parts = textParts(card);
|
|
13761
|
-
const aria = normalizeText(anchor.getAttribute("aria-label"));
|
|
13762
|
-
const heading = normalizeText(card?.querySelector('.qBF1Pd, .fontHeadlineSmall, [role="heading"]')?.textContent);
|
|
13763
|
-
const name = aria ?? heading ?? parts[0] ?? stableUrl;
|
|
13764
|
-
const links = Array.from(card?.querySelectorAll("a[href]") ?? []);
|
|
13765
|
-
const websiteUrl = links.find((link) => link.href.startsWith("http") && !link.href.includes("google."))?.href ?? null;
|
|
13766
|
-
const rating = firstMatching(parts, /^\d(?:\.\d)?$/);
|
|
13767
|
-
const reviewCountRaw = firstMatching(parts, /^\(?[\d,]+\)?(?:\s+reviews?)?$/i);
|
|
13768
|
-
const phone = firstMatching(parts, /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/);
|
|
13769
|
-
const hoursStatus = parts.find((part) => /^(open|closed|closes|opens)\b|^·\s*(opens|closes)\b/i.test(part)) ?? null;
|
|
13770
|
-
const address = parts.find((part) => /\b[A-Z]{2}\s+\d{5}\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Drive|Dr)\b/i.test(part)) ?? null;
|
|
13771
|
-
const directionsUrl = links.find((link) => /google\.[^/]+\/maps\/dir|\/dir\//i.test(link.href))?.href ?? `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", ") || name)}`;
|
|
13772
|
-
const excluded = normalizedSet([name, rating, reviewCountRaw, phone, hoursStatus, address, "Website", "Directions"]);
|
|
13773
|
-
const category = parts.find((part) => {
|
|
13774
|
-
const normalized = part.toLowerCase();
|
|
13775
|
-
return !excluded.has(normalized) && !/^\d(?:\.\d)?$|^\(?[\d,]+\)?(?:\s+reviews?)?$/i.test(part) && !/(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/.test(part) && !/\b[A-Z]{2}\s+\d{5}\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Drive|Dr)\b/i.test(part) && !/^(open|closed|closes|opens)\b|^·\s*(opens|closes)\b|directions|website|book online|sponsored|visit site|financing/i.test(part);
|
|
13776
|
-
}) ?? null;
|
|
13777
|
-
const { cid, cidDecimal } = cidFromUrl(placeUrl);
|
|
13778
|
-
out.push({
|
|
13779
|
-
position: out.length + 1,
|
|
13780
|
-
name,
|
|
13781
|
-
placeUrl,
|
|
13782
|
-
cid,
|
|
13783
|
-
cidDecimal,
|
|
13784
|
-
rating,
|
|
13785
|
-
reviewCount: reviewCountRaw ? reviewCountRaw.replace(/[()]/g, "") : null,
|
|
13786
|
-
category,
|
|
13787
|
-
address,
|
|
13788
|
-
phone,
|
|
13789
|
-
hoursStatus,
|
|
13790
|
-
websiteUrl,
|
|
13791
|
-
directionsUrl,
|
|
13792
|
-
metadata: parts.slice(0, 20)
|
|
13793
|
-
});
|
|
13794
|
-
}
|
|
13795
|
-
return out;
|
|
13813
|
+
const visible = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).find((dialog) => dialog.offsetParent !== null);
|
|
13814
|
+
if (!visible) return { services: [], areasServed: [] };
|
|
13815
|
+
const values = Array.from(visible.querySelectorAll("button")).map((button) => button.innerText.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
13816
|
+
const servicesText = values.find((value) => /^services\s*:/i.test(value));
|
|
13817
|
+
const services = servicesText ? [...new Set(servicesText.replace(/^services\s*:\s*/i, "").split(/\s*[,·]\s*/).map((value) => value.trim()).filter(Boolean))] : [];
|
|
13818
|
+
const served = values.find((value) => /^serves\b/i.test(value));
|
|
13819
|
+
const areasServed = served ? [served.replace(/^serves\s*/i, "").trim()].filter(Boolean) : [];
|
|
13820
|
+
return { services, areasServed };
|
|
13796
13821
|
});
|
|
13797
13822
|
}
|
|
13798
13823
|
};
|
|
@@ -14709,6 +14734,7 @@ async function searchCity(options, market) {
|
|
|
14709
14734
|
gl: options.gl,
|
|
14710
14735
|
hl: options.hl,
|
|
14711
14736
|
maxResults: options.maxResultsPerCity,
|
|
14737
|
+
includeServices: false,
|
|
14712
14738
|
headless: options.headless,
|
|
14713
14739
|
kernelApiKey: options.kernelApiKey,
|
|
14714
14740
|
proxyMode: options.proxyMode,
|
|
@@ -26978,7 +27004,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
26978
27004
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
26979
27005
|
return c.json({ error: "Unauthorized" }, 401);
|
|
26980
27006
|
}
|
|
26981
|
-
const { drainQueue } = await import("./worker-
|
|
27007
|
+
const { drainQueue } = await import("./worker-RYP5EOV5.js");
|
|
26982
27008
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
26983
27009
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
26984
27010
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
|
|
@@ -27180,4 +27206,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
27180
27206
|
export {
|
|
27181
27207
|
app
|
|
27182
27208
|
};
|
|
27183
|
-
//# sourceMappingURL=server-
|
|
27209
|
+
//# sourceMappingURL=server-YXVSZ3T7.js.map
|