← 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 };