← back to La Socrata Ingester

scripts/enrich-rentv-linkedin.js

49 lines

// Enrich rentv_licensed_targets LinkedIn via openclaw ($0 — real logged-in Chrome, the only
// reliable route since these RE pros are ~99% site-less so site-mining/curl-search can't reach
// them). Reuses the proven searchLinkedin (Google in a real browser + precision guards).
// PRIORITIZED: commercial + near-LA (within_300mi) first — that's RENTV's actual deal universe.
// Resumable (contacts_enriched_at stamp). SINGLE worker (one openclaw browser, serialized).
//
// Usage: node scripts/enrich-rentv-linkedin.js [--loop] [--batch=N] [--limit=N]
import { q, pool } from '../src/db.js';
import { searchLinkedin } from './linkedin-openclaw.js';

const flags = new Set(process.argv.slice(2).filter(a => a.startsWith('--') && !a.includes('=')));
const kv = Object.fromEntries(process.argv.slice(2).filter(a => a.includes('=')).map(a => a.slice(2).split('=')));
const BATCH = Number(kv.batch || 40);
const LIMIT = kv.limit ? Number(kv.limit) : null;
const DELAY = Number(process.env.RENTV_DELAY_MS || 400);
const sleep = ms => new Promise(r => setTimeout(r, ms));

async function pick() {
  // dedupe by firm: search each distinct entity_name ONCE, then propagate to all its rows
  return (await q(`SELECT entity_name, max(city) city FROM rentv_licensed_targets
    WHERE linkedin IS NULL AND contacts_enriched_at IS NULL AND entity_name IS NOT NULL AND entity_name <> ''
    GROUP BY entity_name ORDER BY entity_name LIMIT ${BATCH}`)).rows;
}

async function main() {
  const total = Number((await q(`SELECT count(DISTINCT entity_name) c FROM rentv_licensed_targets WHERE linkedin IS NULL AND contacts_enriched_at IS NULL AND entity_name IS NOT NULL AND entity_name<>''`)).rows[0].c);
  console.log(`rentv LinkedIn enrichment via openclaw — ${total.toLocaleString()} distinct firms to process`);
  let done = 0, li = 0;
  for (;;) {
    const batch = await pick();
    if (!batch.length) { console.log('\n✔ rentv LinkedIn enrichment complete'); break; }
    for (const r of batch) {
      let link = null;
      try { link = await searchLinkedin(r.entity_name, r.city); } catch {}
      // propagate to EVERY row of this firm (dedupe)
      const n = (await q(`UPDATE rentv_licensed_targets SET linkedin=$2::text,
                 linkedin_source=CASE WHEN $2::text IS NOT NULL THEN 'openclaw' ELSE linkedin_source END,
                 contacts_enriched_at=now() WHERE entity_name=$1 AND contacts_enriched_at IS NULL`, [r.entity_name, link])).rowCount;
      done++; if (link) { li++; console.log(`  [${done}] ${String(r.entity_name).slice(0, 34).padEnd(34)} → ${link}${n > 1 ? ` (×${n})` : ''}`); }
      if (LIMIT && done >= LIMIT) { console.log(`\nreached --limit=${LIMIT}: ${done} processed, ${li} linkedin. $0.`); return; }
      await sleep(DELAY);
    }
    console.log(`— ${done} processed · ${li} linkedin · ~${(total - done).toLocaleString()} left — $0`);
    if (!flags.has('--loop')) break;
  }
  console.log(`\nTotal: ${done} processed, ${li} linkedin. $0 (openclaw real browser).`);
}
main().catch(e => { console.error('enrich-rentv-linkedin error:', e.message); process.exitCode = 1; }).finally(() => pool.end());