[object Object]

← back to Dw Launches

Vendor-IG: multi-mirror scraper (picuki/picnob/pixwox); keep only cacheable-image posts (picnob URLs ephemeral)

18fb32f20fa6aa19f46ec0aa349a872329aec32f · 2026-06-20 09:14:31 -0700 · Steve

Files touched

Diff

commit 18fb32f20fa6aa19f46ec0aa349a872329aec32f
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 20 09:14:31 2026 -0700

    Vendor-IG: multi-mirror scraper (picuki/picnob/pixwox); keep only cacheable-image posts (picnob URLs ephemeral)
---
 ingest-browserbase.js | 51 +++++++++++++++++++++++++++++++++++----------------
 1 file changed, 35 insertions(+), 16 deletions(-)

diff --git a/ingest-browserbase.js b/ingest-browserbase.js
index b83e9a0..012c38e 100644
--- a/ingest-browserbase.js
+++ b/ingest-browserbase.js
@@ -31,28 +31,30 @@ function pickHandles(argv) {
   return all.filter(h => want.has(h.handle.toLowerCase()));
 }
 
-// IG itself login-walls public profiles; Picuki renders them without login and
-// is scrapable via the cloud browser. We pull each post's thumbnail (Picuki CDN
-// proxies the real IG image), its caption, and the Picuki media permalink.
-async function scrapeProfile(page, handle) {
-  const resp = await page.goto(`https://www.picuki.com/profile/${handle}`, { waitUntil: 'domcontentloaded', timeout: 45000 });
-  if (resp && resp.status() >= 400) throw new Error('picuki ' + resp.status());
-  await page.waitForTimeout(3000);
-  try { await page.click('text=/Accept|Agree|Consent/i', { timeout: 2000 }); } catch {}
-  // scroll to trigger lazy-loaded thumbnails, then settle
+// IG login-walls public profiles; Picuki-family mirrors render them without
+// login and are scrapable via the cloud browser (they proxy the real IG image).
+// They're clones with the same DOM, so one extractor works across all of them.
+// We try each mirror until one yields posts.
+const MIRRORS = [
+  h => `https://www.picuki.com/profile/${h}`,
+  h => `https://www.picnob.com/profile/${h}/`,
+  h => `https://www.pixwox.com/profile/${h}/`
+];
+async function extractPosts(page, max) {
+  try { await page.click('text=/Accept|Agree|Consent/i', { timeout: 1500 }); } catch {}
   try { await page.evaluate(() => window.scrollBy(0, 1600)); } catch {}
-  await page.waitForTimeout(2500);
+  await page.waitForTimeout(2200);
   return await page.evaluate((max) => {
-    const out = [];
-    const seen = new Set();
-    document.querySelectorAll('a[href*="/media/"]').forEach(a => {
+    const out = []; const seen = new Set();
+    // picuki uses /media/<id>; picnob/pixwox use /post/<id> or /p/<code>
+    document.querySelectorAll('a[href*="/media/"], a[href*="/post/"], a[href*="/p/"]').forEach(a => {
       const href = a.href;
-      const id = (href.match(/\/media\/(\d+)/) || [])[1];
+      const id = (href.match(/\/(?:media|post|p)\/([A-Za-z0-9_-]+)/) || [])[1];
       if (!id || seen.has(id)) return;
       const img = a.querySelector('img') || a.parentElement.querySelector('img');
       const src = img && (img.getAttribute('src') || (img.getAttribute('srcset') || '').split(' ')[0]);
       if (!src || !/^https?:/.test(src)) return;
-      const box = a.closest('.box-photo, .item, li, div');
+      const box = a.closest('.box-photo, .item, li, article, div');
       const desc = box && box.querySelector('.photo-description, .description, .caption');
       seen.add(id);
       out.push({
@@ -62,7 +64,24 @@ async function scrapeProfile(page, handle) {
       });
     });
     return out.slice(0, max);
-  }, MAX_POSTS);
+  }, max);
+}
+async function scrapeProfile(page, handle) {
+  let lastErr = 'no mirror yielded posts';
+  for (const m of MIRRORS) {
+    try {
+      const resp = await page.goto(m(handle), { waitUntil: 'domcontentloaded', timeout: 40000 });
+      if (resp && resp.status() >= 400) { lastErr = 'mirror ' + resp.status(); continue; }
+      await page.waitForTimeout(2800);
+      if (/no posts|user has no|not found/i.test(await page.evaluate(() => document.body.innerText.slice(0, 400)))) {
+        lastErr = 'empty profile'; continue;
+      }
+      const posts = await extractPosts(page, MAX_POSTS);
+      if (posts.length) return posts;
+      lastErr = '0 posts extracted';
+    } catch (e) { lastErr = e.message.slice(0, 50); }
+  }
+  throw new Error(lastErr);
 }
 
 (async () => {

← 5d0e46a Compose deep-link: accept ?compose/title/img/link/vendor to  ·  back to Dw Launches  ·  Vendor-IG: in-session screenshot capture (DTD verdict B) — + 51a9575 →