#!/usr/bin/env node /** * EMEX HTTP-only VIN decode test * Tests replacing Playwright scraper with pure fetch + HTML parsing * Run: node scripts/emex-http-test.js */ const vin = process.argv[2] || "NM417800006410193"; const BASE = "https://emexdwc.ae"; const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; async function fetchHtml(url) { const t0 = Date.now(); const res = await fetch(url, { headers: { "User-Agent": UA, "Accept": "text/html,application/xhtml+xml" }, redirect: "follow", }); const html = await res.text(); const ms = Date.now() - t0; console.log(` [${res.status}] ${url.replace(BASE, "")} — ${html.length} bytes, ${ms}ms`); return { html, status: res.status, ms }; } /** * Parse Vehicles.aspx response — extract vehicle list */ function parseVehiclesList(html) { // Match // LABEL const linkRx = /href="(Vehicle\.aspx\?[^"]+)">([^<]+)<\/a>/g; const seen = new Set(); const vehicles = []; let m; while ((m = linkRx.exec(html)) !== null) { const href = m[1].replace(/&/g, "&"); const label = m[2].trim(); if (seen.has(href)) continue; seen.add(href); const params = new URLSearchParams(href.replace("Vehicle.aspx?", "")); const c = params.get("c"); const vid = params.get("vid"); const ssd = params.get("ssd"); // Parse model and year range from label like "SIENA [SIENA EXTRA EUROPA REST.(2002-2012)]" const modelMatch = label.match(/^([^\[]+)/); const yearMatch = label.match(/\((\d{4})/); vehicles.push({ label, catalogCode: c, vid, ssd, model: modelMatch ? modelMatch[1].trim() : label, yearFrom: yearMatch ? parseInt(yearMatch[1]) : null, quickGroupsUrl: c && vid != null && ssd ? `${BASE}/QuickGroups.aspx?c=${c}&vid=${vid}&ssd=${encodeURIComponent(ssd)}` : null, }); } return vehicles; } /** * Parse QuickGroups.aspx response — extract flat category list */ function parseCategories(html) { // Category links go to QuickDetails.aspx?c=X&gid=Y&vid=Z&ssd=... const catRx = /href="(QuickDetails\.aspx\?[^"]+)">([^<]+)<\/a>/g; const seen = new Set(); const cats = []; let m; while ((m = catRx.exec(html)) !== null) { const href = m[1].replace(/&/g, "&"); const label = m[2].trim(); if (label.length < 2 || seen.has(href)) continue; seen.add(href); const params = new URLSearchParams(href.replace("QuickDetails.aspx?", "")); cats.push({ gid: params.get("gid"), name: label, url: `${BASE}/${href}`, }); } return cats; } /** * Determine brand from catalog code (EMEX catalog code → brand) */ function brandFromCatalogCode(c) { if (!c) return null; const map = { BMW: "BMW", MB: "Mercedes-Benz", AU: "Audi", VW: "Volkswagen", FIAT: "Fiat", RFIAT: "Alfa Romeo", FORD: "Ford", RENAULT: "Renault", TOYOTA: "Toyota", HONDA: "Honda", KIA: "Kia", HYUNDAI: "Hyundai", PORSCHE: "Porsche", SUBARU: "Subaru", MAZDA: "Mazda", PEUGEOT: "Peugeot", CPSA: "Citroën/Peugeot", }; for (const [prefix, brand] of Object.entries(map)) { if (c.toUpperCase().includes(prefix)) return brand; } // Extract letters from code like "FFIAT84" → FIAT const letterMatch = c.replace(/[0-9]/g, "").replace(/^[A-Z]/, ""); return letterMatch || c; } async function decodeVin(vin) { console.log(`\n=== EMEX HTTP VIN Decode: ${vin} ===`); const t0 = Date.now(); // Step 1: VIN search — no session/login needed! const { html: vinHtml } = await fetchHtml(`${BASE}/Vehicles.aspx?ft=findByVIN&c=&ssd=&vin=${vin}`); const vehicles = parseVehiclesList(vinHtml); console.log(`\nFound ${vehicles.length} vehicle(s):`); vehicles.forEach((v, i) => console.log(` [${i}] ${v.label} (c=${v.catalogCode})`)); if (vehicles.length === 0) { console.log("\n✗ No vehicles found — VIN not in EMEX database"); return null; } const v = vehicles[0]; const brand = brandFromCatalogCode(v.catalogCode); console.log(`\nVehicle data:`); console.log(` Brand: ${brand}`); console.log(` Model: ${v.model}`); console.log(` Year: ${v.yearFrom}`); console.log(` CatCode: ${v.catalogCode}`); console.log(` QG URL: ${v.quickGroupsUrl}`); // Step 2: Fetch categories from QuickGroups.aspx let categories = []; if (v.quickGroupsUrl) { const { html: qgHtml } = await fetchHtml(v.quickGroupsUrl); categories = parseCategories(qgHtml); console.log(`\nCategories (${categories.length}):`); categories.slice(0, 10).forEach((c) => console.log(` [${c.gid}] ${c.name}`)); if (categories.length > 10) console.log(` ... and ${categories.length - 10} more`); } const total = Date.now() - t0; console.log(`\n✓ Total time: ${total}ms`); console.log(` (Playwright baseline was ~10-20s)`); return { brand, model: v.model, year: v.yearFrom, catalogCode: v.catalogCode, ssd: v.ssd, quickGroupsUrl: v.quickGroupsUrl, categories }; } decodeVin(vin).catch(console.error);