← back to Commercialrealestate
scripts/lib/email-hunt-browser.js
56 lines
/*
* email-hunt-browser.js — the headless-Chrome escalation tier for the email hunt ($0, local).
* When plain fetch strikes out (403 anti-bot or JS-rendered contact info), a real browser
* fingerprint + JS execution recovers a big chunk of the misses (e.g. steppcommercial.com's
* agent emails only exist after hydration). Shared browser instance, reused across calls.
* Requiring this module is safe everywhere; loadable() says whether Playwright actually exists
* (Kamatera prod has no Playwright → callers just skip the browser tier).
*/
'use strict';
const path = require('path');
const os = require('os');
const fs = require('fs');
const { UA } = require('./email-hunt');
let _browser = null, _pw = null;
function loadPlaywright() {
const cands = [
path.join(os.homedir(), 'Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/node_modules/playwright'),
'playwright',
];
for (const c of cands) { try { return require(c); } catch {} }
throw new Error('playwright not found — install it or drop the browser tier');
}
function loadable() { try { loadPlaywright(); return true; } catch { return false; } }
const CHROME_APP = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const CHROME_SHELL = os.homedir() + '/Library/Caches/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-mac-arm64/chrome-headless-shell';
async function launchBrowser() {
const opts = { headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] };
// Prefer REAL system Chrome (best anti-bot fingerprint); fall back to the installed headless-shell.
if (fs.existsSync(CHROME_APP)) { try { return await _pw.chromium.launch({ ...opts, executablePath: CHROME_APP }); } catch {} }
if (fs.existsSync(CHROME_SHELL)) { try { return await _pw.chromium.launch({ ...opts, executablePath: CHROME_SHELL }); } catch {} }
return await _pw.chromium.launch(opts);
}
async function browserHtml(url, ms = 20000) {
try {
if (!_pw) _pw = loadPlaywright();
if (!_browser) _browser = await launchBrowser();
// ignoreHTTPSErrors: small broker sites run expired certs (mdrealtycorp.com) — read-only
// public-page scrape, same lenient-TLS stance as email-hunt's getInsecure fallback.
const ctx = await _browser.newContext({ userAgent: UA, viewport: { width: 1280, height: 900 }, ignoreHTTPSErrors: true });
const page = await ctx.newPage();
let html = '';
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: ms });
await page.waitForTimeout(1800); // let JS hydrate contact widgets
html = await page.content();
} catch (e) { /* return whatever rendered */ try { html = await page.content(); } catch {} }
await ctx.close();
return { ok: !!html, status: html ? 200 : 0, html: (html || '').slice(0, 800000), finalUrl: url };
} catch (e) { return { ok: false, status: 0, err: e.message.split('\n')[0], html: '', finalUrl: url }; }
}
async function closeBrowser() { try { if (_browser) await _browser.close(); } catch {} _browser = null; }
module.exports = { loadable, browserHtml, closeBrowser };