[object Object]

← back to Rentv Ad Engine

Integrate RENTV podcast (SoundCloud rentvnews) + The REview authoritative source: harvest-rentvreview.py maps 7 episodes with official sponsors/guest/date; dossier shows 'Verified via The REview' block; header podcast+REview links

750f51fe653974b1b22d36d11aa64e8373f7054c · 2026-08-07 09:43:25 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 750f51fe653974b1b22d36d11aa64e8373f7054c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 09:43:25 2026 -0700

    Integrate RENTV podcast (SoundCloud rentvnews) + The REview authoritative source: harvest-rentvreview.py maps 7 episodes with official sponsors/guest/date; dossier shows 'Verified via The REview' block; header podcast+REview links
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 data/rentvreview.json          | 78 ++++++++++++++++++++++++++++++++++
 public/index.html              | 14 +++++-
 scripts/harvest-rentvreview.py | 96 ++++++++++++++++++++++++++++++++++++++++++
 server.js                      |  1 +
 4 files changed, 188 insertions(+), 1 deletion(-)

diff --git a/data/rentvreview.json b/data/rentvreview.json
new file mode 100644
index 0000000..92e303a
--- /dev/null
+++ b/data/rentvreview.json
@@ -0,0 +1,78 @@
+{
+ "771348410": {
+  "rv_id": "1909",
+  "url": "https://www.rentvreview.com/Videos/View/1909",
+  "vimeo": "771348410",
+  "title": "Commercial Real Estate Talk, Episode #1 - Bill Shopoff 9677 views Video added by Commercia",
+  "sponsors": [],
+  "date": "November 14, 2022",
+  "guest_note": "Bill Shopoff \u2014 President and CEO of Shopoff Realty Investments, which is celebrating its 30th Anniversar"
+ },
+ "795355693": {
+  "rv_id": "1910",
+  "url": "https://www.rentvreview.com/Videos/View/1910",
+  "vimeo": "795355693",
+  "title": "Commercial Real Estate Talk, Episode #2 - Sandy Sigal 7118 views Video added by Commercial",
+  "sponsors": [],
+  "date": "January 30, 2023",
+  "guest_note": "Sandy SIgal \u2014 President and CEO of NewMark Merrill Companies"
+ },
+ "953281926": {
+  "rv_id": "2493",
+  "url": "https://www.rentvreview.com/Videos/View/2493",
+  "vimeo": "953281926",
+  "title": "RENTV CRE Talk Episode #11 - Michael Rademaker 9942 views Video added by Commercial Real E",
+  "sponsors": [
+   "Paramount Property Tax Appeal",
+   "Commercial Real Estate Inspectors",
+   "Fidelity Mortgage Lenders"
+  ],
+  "date": "June 03, 2024",
+  "guest_note": "Michael Rademaker \u2014 Founder and CEO of MGR Real Estate, his Ontario-based company the has a residential and"
+ },
+ "1039101858": {
+  "rv_id": "2642",
+  "url": "https://www.rentvreview.com/Videos/View/2642",
+  "vimeo": "1039101858",
+  "title": "RENTV CRE Talk Episode #13 - Ethan Penner 4876 views Video added by Commercial Real Estate",
+  "sponsors": [
+   "Chase Partners",
+   "Commercial Real Estate Inspectors",
+   "Fidelity Mortgage Lenders"
+  ],
+  "date": "December 13, 2024",
+  "guest_note": "Ethan Penner \u2014 Founder & CEO of Mosaic Real Estate Investors, an iconic figure in the real estate fina"
+ },
+ "1056513923": {
+  "rv_id": "2726",
+  "url": "https://www.rentvreview.com/Videos/View/2726",
+  "vimeo": "1056513923",
+  "title": "RENTV CRE Talk Episode #14 - John McNellis 3573 views Video added by Commercial Real Estat",
+  "sponsors": [],
+  "date": "February 12, 2025",
+  "guest_note": "John McNellis \u2014 Partner at McNellis Partners, an owner, investor and developer of shopping centers, mai"
+ },
+ "1200839794": {
+  "rv_id": "3255",
+  "url": "https://www.rentvreview.com/Videos/View/3255",
+  "vimeo": "1200839794",
+  "title": "CRE Talk Episode #22 with Bill Shopoff 2034 views Video added by Commercial Real Estate Ta",
+  "sponsors": [
+   "Rockefeller Group",
+   "Chase Partners",
+   "Provident Savings Bank",
+   "Fidelity Mortgage Lenders"
+  ],
+  "date": "June 10, 2026",
+  "guest_note": "Bill Shopoff \u2014 President and CEO of Shopoff Realty Investments, a very active developer of major long-te"
+ },
+ "852462696": {
+  "rv_id": "2033",
+  "url": "https://www.rentvreview.com/Videos/View/2033",
+  "vimeo": "852462696",
+  "title": "RENTV CRE Talk Episode #5 Barry DiRaimondo 14679 views Video added by Commercial Real Esta",
+  "sponsors": [],
+  "date": "August 09, 2023",
+  "guest_note": ""
+ }
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index ab7910b..4d1e23d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -67,6 +67,10 @@
     <div class="tab" data-view="coverage">Coverage</div>
   </div>
   <div class="stat" id="stat"></div>
+  <div style="margin-left:auto;display:flex;gap:10px;font-size:12px">
+    <a href="https://soundcloud.com/rentvnews" target="_blank" rel="noopener" title="RENTV podcast on SoundCloud">🎧 Podcast</a>
+    <a href="https://www.rentvreview.com" target="_blank" rel="noopener" title="The REview — RENTV's authoritative episode archive">📺 The REview</a>
+  </div>
 </header>
 
 <div class="toolbar">
@@ -96,7 +100,7 @@ const setLS=(k,v)=>localStorage.setItem("rae_"+k,JSON.stringify(v));
 const siteURL=s=>/^https?:/.test(s)?s:"https://"+s;
 const telURL=p=>"tel:"+String(p).replace(/[^\d+]/g,"");
 
-let DATA={advertisers:[],guests:[],videos:[],emails:[]}, FRAMES={}, SYN={};
+let DATA={advertisers:[],guests:[],videos:[],emails:[]}, FRAMES={}, SYN={}, RV={};
 let view=LS("view","advertisers"), mode=LS("mode","cards");
 
 const SORTS={
@@ -124,6 +128,7 @@ async function boot(){
   (fr.frames||[]).forEach(f=>FRAMES[f.id]=f);
   try{SYN=await fetch("/api/synopses").then(r=>r.json())}catch{SYN={}}
   try{DATA.emails=(await fetch("/api/emails").then(r=>r.json())).emails||[]}catch{DATA.emails=[]}
+  try{RV=await fetch("/api/rentvreview").then(r=>r.json())}catch{RV={}}
   $("#stat").innerHTML=`<b>${s.advertisers}</b> advertisers · <b>${s.guests}</b> guests · <b>${s.videos_rich}</b>/${s.videos_total} videos transcribed`;
   $("#dens").value=LS("dens",4); document.documentElement.style.setProperty("--cols",$("#dens").value);
   document.querySelectorAll(".tab").forEach(t=>t.classList.toggle("on",t.dataset.view===view));
@@ -341,6 +346,13 @@ window.openEpisode=id=>{
     <h3>Synopsis</h3>
     <p>${esc(syn.synopsis||"(synopsis still generating — refresh shortly)")}</p>
     ${(syn.highlights||[]).length?`<ul style="color:var(--dim)">${syn.highlights.map(h=>`<li>${esc(h)}</li>`).join("")}</ul>`:""}
+    ${RV[id]?`<div class="mailpanel" style="border-color:var(--gold)">
+      <div class="row" style="justify-content:space-between"><b style="color:var(--gold)">✓ Verified via The REview (RENTV official)</b>
+        <a href="${esc(RV[id].url)}" target="_blank" rel="noopener">open ↗</a></div>
+      ${RV[id].date?`<div class="kv">📅 ${esc(RV[id].date)}</div>`:""}
+      ${RV[id].guest_note?`<div class="kv">🎤 ${esc(RV[id].guest_note)}</div>`:""}
+      ${(RV[id].sponsors||[]).length?`<div class="row"><span class="kv">Official sponsors:</span>${RV[id].sponsors.map(s=>`<span class="chip" style="color:var(--gold);border-color:#3a3320">${esc(s)}</span>`).join("")}</div>`:""}
+    </div>`:""}
     <h3>Advertisers in this episode (${ads.length})</h3>
     ${ads.length?ads.map(a=>`<div class="row" style="margin:4px 0"><b>${esc(a.name)}</b> <span class="badge cat">${esc(a.category)}</span>
         ${(a.websites||[]).map(w=>`<a href="${siteURL(esc(w))}" target="_blank" rel="noopener">${esc(w)}</a>`).join(" ")}
diff --git a/scripts/harvest-rentvreview.py b/scripts/harvest-rentvreview.py
new file mode 100644
index 0000000..b17990c
--- /dev/null
+++ b/scripts/harvest-rentvreview.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+"""Harvest AUTHORITATIVE episode data from rentvreview.com ("The REview").
+
+Each /Videos/View/<id> page is server-rendered and carries, in plain HTML:
+  - data-video-external-id="<vimeoId>"  (maps REview episode -> our Vimeo video)
+  - "... Sponsored by: A; B; and C. To be a part ..."  (authoritative sponsor list)
+  - the guest/synopsis prose + "Date Created: <date>"
+This is cleaner than transcript inference and covers episodes predating the Vimeo feed.
+
+We BFS from known CRE Talk pages, following the 6 related-video links per page, keeping
+pages whose Vimeo id matches our library OR whose title is a CRE Talk episode. Free,
+bounded (MAX_FETCH), no API/DRM.
+
+Output: data/rentvreview.json  { "<vimeoId>": {rv_id,url,sponsors[],guest_note,date,title} }
+Env: MAX_FETCH (default 120), SEEDS (comma ids)
+"""
+import json, os, re, time, urllib.request, html as _html
+from collections import deque
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+MANIFEST = os.path.join(ROOT, "data", "manifest.json")
+OUT = os.path.join(ROOT, "data", "rentvreview.json")
+UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1"
+MAX_FETCH = int(os.environ.get("MAX_FETCH", "120"))
+SEEDS = [s for s in os.environ.get("SEEDS", "1909,1910,2493,2642,2726,3255,1034,1650,2033,2298,2898,3273").split(",") if s]
+
+VIMEO_IDS = {str(v["id"]) for v in json.load(open(MANIFEST))["videos"]}
+
+
+def fetch(rv_id):
+    try:
+        return urllib.request.urlopen(urllib.request.Request(
+            f"https://www.rentvreview.com/Videos/View/{rv_id}", headers={"User-Agent": UA}), timeout=20
+        ).read().decode("utf-8", "ignore")
+    except Exception:
+        return ""
+
+
+def parse(h):
+    ext = re.search(r'data-video-external-id="(\d+)"', h)
+    vimeo = ext.group(1) if ext else ""
+    text = _html.unescape(re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", h)))
+    title = ""
+    mt = re.search(r"(RENTV CRE Talk Episode #?\d+[^|<]*|CRE Talk,? Episode #?\d+[^|<]*|Commercial Real Estate Talk,? Episode #?\d+[^|<]*)", text)
+    if mt:
+        title = mt.group(1).strip()[:90]
+    sponsors = []
+    ms = re.search(r"Sponsored by[:\s]+(.+?)\.\s*(?:To be a part|If you|Thank you|$)", text, re.I)
+    if ms:
+        raw = ms.group(1)
+        parts = re.split(r";|,|\band\b", raw)
+        sponsors = [re.sub(r"\s+", " ", p).strip().title() for p in parts if p.strip() and len(p.strip()) > 2]
+    date = ""
+    md = re.search(r"Date Created:\s*([A-Za-z]+ \d{1,2}, \d{4})", text)
+    if md:
+        date = md.group(1)
+    guest = ""
+    mg = re.search(r"interview\s+([A-Z][a-zA-Z.\-]+ [A-Z][a-zA-Z.\-]+)[,]?\s+((?:President|CEO|Founder|Partner|Managing|Chief|Head|Owner|Chairman)[^.]{0,80})", text)
+    if mg:
+        guest = f"{mg.group(1)} — {mg.group(2).strip()}"[:120]
+    related = re.findall(r'href="/Videos/View/(\d+)"', h)
+    return {"vimeo": vimeo, "title": title, "sponsors": sponsors, "date": date, "guest_note": guest}, related
+
+
+def main():
+    out = json.load(open(OUT)) if os.path.exists(OUT) else {}
+    seen, q, fetched = set(), deque(SEEDS), 0
+    print(f"[rentvreview] BFS from {len(SEEDS)} seeds, cap {MAX_FETCH}, matching {len(VIMEO_IDS)} vimeo ids")
+    while q and fetched < MAX_FETCH:
+        rv = q.popleft()
+        if rv in seen:
+            continue
+        seen.add(rv)
+        h = fetch(rv)
+        fetched += 1
+        if not h:
+            continue
+        rec, related = parse(h)
+        is_cre = ("Commercial Real Estate Talk" in h or "CRE Talk" in h)
+        if rec["vimeo"] and (rec["vimeo"] in VIMEO_IDS or is_cre):
+            key = rec["vimeo"]
+            out[key] = {"rv_id": rv, "url": f"https://www.rentvreview.com/Videos/View/{rv}", **rec}
+            # follow related to find sibling episodes
+            for r in related:
+                if r not in seen:
+                    q.append(r)
+        if fetched % 15 == 0:
+            print(f"  fetched={fetched} matched={len(out)} queue={len(q)}")
+        time.sleep(0.3)
+    json.dump(out, open(OUT, "w"), indent=1)
+    matched_lib = sum(1 for k in out if k in VIMEO_IDS)
+    print(f"[rentvreview] DONE fetched={fetched} matched={len(out)} (of which {matched_lib} in our Vimeo library) -> {OUT}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/server.js b/server.js
index c6ded5f..c5120d6 100644
--- a/server.js
+++ b/server.js
@@ -40,6 +40,7 @@ const server = http.createServer((req, res) => {
   if (p === "/api/frames") return send(res, 200, readJSON("frames.json", { frames: [] }));
   if (p === "/api/synopses") return send(res, 200, readJSON("synopses.json", {}));
   if (p === "/api/emails") return send(res, 200, readJSON("emails.json", { emails: [] }));
+  if (p === "/api/rentvreview") return send(res, 200, readJSON("rentvreview.json", {}));
   if (p === "/api/email" && req.method === "POST") return draftEmail(req, res);
   if (p === "/api/health") return send(res, 200, { ok: true, ts: Date.now() });
   if (p === "/api/stats") {

← 6eeb40a Phase 2b: web-search address enrichment — 24/29 advertisers  ·  back to Rentv Ad Engine  ·  Full REview reconciliation: producer-page harvest maps 20/22 c3eae5f →