import * as cheerio from "cheerio"; import type { NormalizedQuery, Offer, OfferAttribute, StoreAdapter } from "../core/types.js"; import { getBrowserHub } from "../core/browserHub.js"; // EUAutodalas.lv — server-rendered results at /meklesana-rezervesdalas?keyword= // (same autoteile-net platform family as rd24; media on euautoteile.de). Was open at the // original probe, moved behind a Cloudflare managed challenge by M2 → onDemand (walled) // store, fetched by navigating the resident headful browser (BrowserHub.fetchPageHtml). // Live-verified 2026-07-12: warm browser on the residential IP passes non-interactively // (200, "148 rezultāti" for W71252); noVNC solve is the cold-session fallback. // // Price extraction: the markup carries decorative `0,00 €` elements (spec §M1 warning) — // we read the clean decimal from `.product_listing_price_banner_wrapper[data-price]` // instead of any price text. Fixture: fixtures/euautodalas/W71252.html (2026-07-12). const HOME = "https://www.euautodalas.lv"; const SEARCH_URL = (q: string) => `${HOME}/meklesana-rezervesdalas?keyword=${encodeURIComponent(q)}`; const SELECTORS = { card: ".cat_items .cat_item", titleLink: "a.name", art: ".art", // "Produktu skaits: " then "OEM numurs: …" then "Stav: …" brandImg: ".i_firm img", // alt = "MANN-FILTER W 712/52" (brand + part number) priceWrapper: ".product_listing_price_banner_wrapper", // data-price="8.43", clean decimal availability: ".i_del .i_del-text", image: ".image img", }; export const euautodalas: StoreAdapter = { id: "euautodalas", displayName: "EUAutodalas.lv", storeHomepage: HOME, enabled: true, fetchMode: "onDemand", timeoutMs: 45000, // navigation + possible challenge auto-clear wait + one re-navigation async fetchHtml(q: NormalizedQuery): Promise { // Walled: navigate the resident Chrome to the results page (see header comment). // The site normalizes the part number itself (W712/52 → W71252), so canonical is fine. return getBrowserHub().fetchPageHtml(this.id, SEARCH_URL(q.canonical)); }, parse(html: string): Offer[] { const $ = cheerio.load(html); const offers: Offer[] = []; $(SELECTORS.card).each((_, el) => { const card = $(el); const link = card.find(SELECTORS.titleLink).first(); const url = link.attr("href") ?? ""; // Anchor text is two
-separated lines: a spec line ("Eļļas filtrs: Augstums: // 92mm, Ø: 76mm, ar atgriezējvārstu") and "Rezerves daļu ražotājs: ". const linkText = link.text().replace(/\s+/g, " ").trim(); const [specLine = ""] = linkText.split(/Rezerves daļu ražotājs:/); const title = specLine.trim().replace(/,\s*$/, ""); const artText = card.find(SELECTORS.art).text(); const partNumber = artText.match(/Produktu skaits:\s*([^\n]+)/)?.[1]?.trim() ?? ""; // Brand image alt is " " — strip the number suffix. const brandAlt = (card.find(SELECTORS.brandImg).attr("alt") ?? "").trim(); const brand = partNumber && brandAlt.endsWith(partNumber) ? brandAlt.slice(0, -partNumber.length).trim() || null : brandAlt || null; const priceEur = Number(card.find(SELECTORS.priceWrapper).attr("data-price")); if (!partNumber || !Number.isFinite(priceEur) || priceEur <= 0) return; // The spec line doubles as the attribute list: "Category: Label: value, Label: // value, flag". Flag-style entries go in value with label "" (types.ts convention). const attributes: OfferAttribute[] = []; const specBody = title.includes(":") ? title.slice(title.indexOf(":") + 1) : ""; for (const part of specBody.split(",")) { const piece = part.trim(); if (!piece) continue; const m = piece.match(/^([^:]+):\s*(.+)$/); if (m?.[1] && m[2]) attributes.push({ label: m[1].trim(), value: m[2].trim() }); else attributes.push({ label: "", value: piece }); } const imgEl = card.find(SELECTORS.image).first(); const imageUrl = imgEl.attr("data-srcset")?.split(" ")[0] ?? imgEl.attr("src") ?? null; offers.push({ store: euautodalas.id, brand, partNumber, title, priceEur, availability: card.find(SELECTORS.availability).first().text().trim() || null, deliveryNote: null, url, imageUrl, attributes, }); }); return offers; }, };