SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
3 days agolast push
HTML 98.9% Python 0.6%
6.1 KB · 144 lines javascript
Raw Blame History
1#!/usr/bin/env node2// -----------------------------------------------------------------------------3// Lou-Ka — kit Figma · shots.mjs4// Capture le site live www.lou-ka.com (desktop 1440 + mobile 390) et rapatrie5// quelques photos d'annonces réelles pour les maquettes.6//   · references/fold/*.jpg   : au-dessus du pli (embarqué dans le plugin)7//   · references/full/*.jpg   : pleine page (à glisser dans Figma au besoin)8//   · references/photos/*.jpg : photos d'annonces réduites à 640 px9// Usage : node shots.mjs   (Playwright de ~/ka-screenshots réutilisé)10// -----------------------------------------------------------------------------11import { createRequire } from "node:module";12import { mkdirSync, writeFileSync } from "node:fs";13import { execSync } from "node:child_process";14import path from "node:path";1516const require = createRequire(path.join(process.env.HOME, "ka-screenshots/node_modules/"));17const { chromium } = require("playwright");1819const ROOT = new URL(".", import.meta.url).pathname;20const REF = path.join(ROOT, "references");21for (const d of ["fold", "full", "photos"]) mkdirSync(path.join(REF, d), { recursive: true });2223const BASE = "https://www.lou-ka.com";2425// ---- 1. Choisir une vraie annonce riche (photos, prix, secteur) -------------26async function pickListing() {27  const r = await fetch(`${BASE}/api/search?city=Montr%C3%A9al&page_size=60&sort=recent`);28  const d = await r.json();29  const ok = d.listings.filter(30    (l) => (l.images?.length ?? 0) >= 6 && l.price >= 1100 && l.price <= 2600 && l.sector && l.unit_type,31  );32  ok.sort((a, b) => (b.fv_verdict ? 1 : 0) - (a.fv_verdict ? 1 : 0) || b.images.length - a.images.length);33  return { pick: ok[0] ?? d.listings[0], pool: d.listings };34}3536// ---- 2. Photos d'annonces (pour les cartes de la maquette) ------------------37async function grabPhotos(pool, pick) {38  const urls = [];39  for (const im of (pick.images ?? []).slice(0, 5)) urls.push(im);40  for (const l of pool) {41    if (urls.length >= 12) break;42    if (l.uid === pick.uid) continue;43    if (l.images?.[0] && l.price >= 900) urls.push(l.images[0]);44  }45  let i = 0;46  const kept = [];47  for (const u of urls) {48    try {49      const res = await fetch(u, { headers: { "User-Agent": "Mozilla/5.0" } });50      if (!res.ok) continue;51      const buf = Buffer.from(await res.arrayBuffer());52      if (buf.length < 8000) continue;53      const raw = path.join(REF, "photos", `raw-${i}.img`);54      writeFileSync(raw, buf);55      const out = path.join(REF, "photos", `photo-${String(i + 1).padStart(2, "0")}.jpg`);56      execSync(`sips -s format jpeg -s formatOptions 78 -Z 640 "${raw}" --out "${out}" >/dev/null 2>&1 && rm -f "${raw}"`);57      kept.push(out);58      i++;59    } catch (e) {60      /* on passe */61    }62  }63  return kept;64}6566// ---- 3. Captures d'écran --------------------------------------------------67const HIDE_CSS = `68  .cookie-banner, .ka-agent, #ka-agent, .ka-agent-btn, [class*="agent-fab"] { display: none !important; }69  .ticker-track { animation: none !important; }70  *, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }71`;7273async function shoot(browser, { name, url, w, h, mobile }) {74  const ctx = await browser.newContext({75    viewport: { width: w, height: h },76    deviceScaleFactor: 2,77    isMobile: !!mobile,78    hasTouch: !!mobile,79    locale: "fr-CA",80    userAgent: mobile81      ? "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1"82      : undefined,83  });84  const page = await ctx.newPage();85  await page.addInitScript(() => {86    try { localStorage.setItem("louka_consent", JSON.stringify({ analytics: false, ts: Date.now() })); } catch {}87  });88  await page.goto(url, { waitUntil: "networkidle", timeout: 60000 }).catch(() => {});89  await page.addStyleTag({ content: HIDE_CSS });90  await page.waitForTimeout(1800);91  // fermer un éventuel bandeau de consentement resté visible92  for (const sel of ["button:has-text('Tout accepter')", "button:has-text('Accepter')", "button:has-text('Refuser')"]) {93    const b = page.locator(sel).first();94    if (await b.count()) { await b.click({ timeout: 1000 }).catch(() => {}); break; }95  }96  await page.evaluate(() => window.scrollTo(0, 0));97  await page.waitForTimeout(400);98  const fold = path.join(REF, "fold", `${name}.jpg`);99  await page.screenshot({ path: fold, type: "jpeg", quality: 80, fullPage: false });100  const full = path.join(REF, "full", `${name}.jpg`);101  await page.screenshot({ path: full, type: "jpeg", quality: 80, fullPage: true }).catch(() => {});102  await ctx.close();103  return fold;104}105106const { pick, pool } = await pickListing();107console.log("Annonce de référence :", pick.uid, "—", pick.title, pick.price, pick.city);108const photos = await grabPhotos(pool, pick);109console.log("Photos récupérées :", photos.length);110111const pages = [112  ["accueil", "/"],113  ["logement", `/logement/${encodeURIComponent(pick.uid)}`],114  ["stats", "/stats"],115  ["villes", "/villes"],116  ["court-terme", "/court-terme"],117];118const browser = await chromium.launch();119const done = [];120for (const [name, p] of pages) {121  done.push(await shoot(browser, { name: `desktop-${name}`, url: BASE + p, w: 1440, h: 900 }));122  done.push(await shoot(browser, { name: `mobile-${name}`, url: BASE + p, w: 390, h: 844, mobile: true }));123  console.log("✓", name);124}125await browser.close();126127writeFileSync(128  path.join(REF, "manifest.json"),129  JSON.stringify(130    {131      capturedAt: new Date().toISOString(),132      listing: { uid: pick.uid, title: pick.title, address: pick.address, sector: pick.sector, city: pick.city,133        unit_type: pick.unit_type, price: pick.price, price_label: pick.price_label, source: pick.source,134        availability: pick.availability, amenities: pick.amenities, bedrooms: pick.bedrooms, bathrooms: pick.bathrooms,135        fv_verdict: pick.fv_verdict, fv_deviation: pick.fv_deviation, ks_global: pick.ks_global, nImages: pick.images?.length },136      photos: photos.map((p) => path.basename(p)),137      shots: done.map((p) => path.basename(p)),138    },139    null,140    2,141  ),142);143console.log("Terminé →", REF);144