← back to Rentv Website

scripts/pull-news.mjs

52 lines

#!/usr/bin/env node
// Live-data engine for rentv-v1: pulls REAL rentv.com news headlines on a schedule.
// Runs via launchd every 20 min; writes data/news.json the site serves. $0 (plain fetch).
import { writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const OUT = join(HERE, '..', 'data'); mkdirSync(OUT, { recursive: true });

async function fetchDecoded(url) {
  const r = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 RENTV-v1' }, signal: AbortSignal.timeout(20000) });
  const buf = Buffer.from(await r.arrayBuffer());
  let s = buf.toString('utf8');
  if ((s.match(/�/g) || []).length > 5) s = buf.toString('latin1'); // WIN-1252 fallback
  return s;
}
const clean = (t) => t.replace(/&/g,'&').replace(/'/g,"'").replace(/"/g,'"').replace(/ /g,' ').replace(/\s+/g,' ').trim();

try {
  const html = await fetchDecoded('https://www.rentv.com/');
  // Map each story id -> its real property photo (linked-image form: <a href='...news/ID'><img src='.../images/dynamic/news_*.jpg'>)
  const imgMap = {};
  const ire = /href=['"][^'"]*\/news\/(\d+)['"]\s*>\s*<img[^>]+src=['"]([^'"]+\/images\/dynamic\/news_[^'"]+\.(?:jpe?g|png))['"]/gi;
  let im; while ((im = ire.exec(html))) { if (!imgMap[im[1]]) imgMap[im[1]] = im[2].startsWith('http') ? im[2] : 'https://www.rentv.com/' + im[2].replace(/^\//, ''); }
  const re = /<a[^>]+href="([^"]*content\/homepage\/mainnews\/news\/(\d+))"[^>]*>([^<]{20,160})<\/a>/gi;
  const seen = new Set(), items = []; let m;
  while ((m = re.exec(html)) && items.length < 30) {
    const id = m[2]; if (seen.has(id)) continue; seen.add(id);
    const title = clean(m[3]); if (title.length < 20) continue;
    // classify by keywords
    const t = title.toLowerCase();
    const cat = /financ|loan|refi|recap|debt/.test(t) ? 'Financing'
      : /lease|sf lease|tenant/.test(t) ? 'Leases'
      : /develop|breaks ground|project|mixed-use/.test(t) ? 'Development'
      : /retail|shop|center/.test(t) ? 'Retail'
      : /res |multifamily|apartment|unit/.test(t) ? 'Multifamily'
      : /industrial|warehouse|logistics/.test(t) ? 'Industrial'
      : 'Sales';
    items.push({ id, title, url: m[1].startsWith('http') ? m[1] : 'https://www.rentv.com/' + m[1].replace(/^\//,''), cat, image: imgMap[id] || '' });
  }
  const payload = { source: 'rentv.com — live', fetched_at: new Date().toISOString(), count: items.length, items };
  writeFileSync(join(OUT, 'news.json'), JSON.stringify(payload, null, 1));
  console.log('OK: pulled ' + items.length + ' live headlines');
} catch (e) {
  console.error('pull failed:', e.message);
  // never overwrite a good cache with nothing — leave last-good in place
  console.error('(news kept last-good; continuing to ticker refresh)');
}

// Refresh the live market ticker on the same cron (runs regardless of news outcome).
try { await import('./pull-ticker.mjs'); } catch (e) { console.error('ticker refresh failed:', e.message); }