← 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());