import type { ConnectorMeta, CrawlContext } from '@rareindex/connectors'; import type { ExtractionResult } from '@rareindex/shared'; import { parseEuMoney } from '../_g8-auctions-eu-apac-lib/index.js'; import { SaleResultsConnector, absolute, chunksBetween, pick, textOf, type HouseConfig, type ParsedLot, type ParsedSalePage, type SaleRef } from '../_g8-auctions-eu-apac-lib/sale-results.js'; /** * Dunbar Sloane (Wellington / Auckland, NZ) — Tandem Auctions platform (auctions.dunbarsloane.co.nz). * /previous-auctions lists past sales (title, "Thursday, 2 - Wednesday, 15 October 2025", /); * //catalogue?page=N shows lot cards (lot number, title, "Estimate: $200 - $350", "Realised: $140 + premium"). * "Realised … plus premium" = HAMMER in NZD. */ const BASE = 'https://auctions.dunbarsloane.co.nz'; /** "Wednesday, 21 June 2023" | "Thursday, 2 - Wednesday, 15 October 2025" | "Friday, 17 - Tuesday, 28 October 2025" → first day. */ export function parseNzDate(text: string | null | undefined): string | null { if (!text) return null; const t = textOf(text).replace(/\s+/g, ' '); const range = t.match(/(\d{1,2})\s*(?:([A-Za-z]{3,9})\s*)?-\s*(?:[A-Za-z]+,\s*)?(\d{1,2})\s+([A-Za-z]{3,9})\s+(\d{4})/); const single = t.match(/(\d{1,2})\s+([A-Za-z]{3,9})\s+(\d{4})/); const parts = range ? { d: range[1]!, mon: range[2] ?? range[4]!, y: range[5]! } : single ? { d: single[1]!, mon: single[2]!, y: single[3]! } : null; if (!parts) return null; const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; const mo = months.indexOf(parts.mon.slice(0, 3).toLowerCase()); if (mo < 0) return null; const d = new Date(Date.UTC(Number(parts.y), mo, Number(parts.d))); return Number.isNaN(d.getTime()) ? null : d.toISOString(); } export function parsePreviousAuctions(htmlText: string): SaleRef[] { const out: SaleRef[] = []; const seen = new Set(); for (const chunk of chunksBetween(htmlText, /
/)) { const id = chunk.match(/href="\/(\d+)"/)?.[1]; if (!id || seen.has(id)) continue; seen.add(id); const title = pick(chunk, /

([\s\S]*?)<\/h2>/) ?? `Auction ${id}`; const dateText = pick(chunk, /

([\s\S]*?)<\/h3>/); const badge = pick(chunk, /([\s\S]*?)<\/span>/); // timed sales run for two weeks: keep the closing day so the crawler waits for results const endText = dateText?.match(/-\s*([A-Za-z]+,\s*\d{1,2}\s+[A-Za-z]{3,9}\s+\d{4})/)?.[1] ?? null; out.push({ id, title, url: `${BASE}/${id}/catalogue`, date: parseNzDate(dateText), location: null, extra: { date_text: dateText, auction_type: badge, end_date: endText ? parseNzDate(endText) : null } }); } return out; } export function parseCataloguePage(htmlText: string, sale: SaleRef, page: number): ParsedSalePage | null { if (!/class="lot-grid/.test(htmlText) && !/data-ln="/.test(htmlText)) return null; const lots: ParsedLot[] = []; for (const chunk of chunksBetween(htmlText, /
([\s\S]*?)/)?.[1] ?? chunk.match(/

([\s\S]*?)<\/p>/)?.[1] ?? ''; const title = textOf(textBlock) || (alt ? textOf(alt) : ''); if (!title) continue; const est = pick(chunk, /\s*Estimate:\s*([^<]*)\s*Realised:\s*\$?\s*([\d,]+)\s*([^<]*<\/small>)?/); const price = realised ? parseEuMoney(`NZ$${realised[1]}`, 'NZD', 'en') : null; const premiumNote = realised?.[2] ? textOf(realised[2]) : null; lots.push({ lotNo, title, subtitle: null, description: null, url: absolute(BASE, href)!, image, price: price?.amount ?? null, currency: 'NZD', premiumIncluded: premiumNote ? !/\+|plus/i.test(premiumNote) : null, estimateLow: estM ? Number(estM[1]!.replace(/,/g, '')) : null, estimateHigh: estM ? Number(estM[2]!.replace(/,/g, '')) : null, date: null, sold: price !== null, extra: { premium_note: premiumNote }, }); } const pageOf = htmlText.match(/Page\s+(\d+)\s+of\s+(\d+)/); const total = pageOf ? Number(pageOf[2]) : null; const header = pick(htmlText, /

([\s\S]*?)([\s\S]*?)<\/small>/); return { lots, hasMore: total !== null ? page < total : false, totalLots: null, sale: { title: header ?? undefined, date: parseNzDate(dateText) ?? undefined, extra: { pages: total } } }; } export class DunbarSloaneConnector extends SaleResultsConnector { readonly version = '1.0.0'; readonly house: HouseConfig = { houseName: 'Dunbar Sloane', defaultCurrency: 'NZD', location: 'Wellington, New Zealand', idKey: 'dunbar_sloane_lot', premiumIncluded: false, fallbackSlug: 'antiques', minIntervalMs: 2500, maxPagesPerSale: 25 }; protected override minIntervalMs = 2500; async listSales(ctx: CrawlContext): Promise { const url = String(this.meta.config.previousUrl ?? `${BASE}/previous-auctions`); await this.throttle(url); const res = await ctx.fetch(url, { engines: ['api'], responseType: 'text', minQuality: 0, timeoutMs: 60_000 }); if (!res.success || !res.html) { ctx.anomaly('past_list_failed', `${url}: ${res.error ?? res.httpStatus}`); return []; } return parsePreviousAuctions(res.html); } salePageUrl(sale: SaleRef, page: number): string { return page > 1 ? `${sale.url}?page=${page}` : sale.url; } parseSalePage(res: ExtractionResult, sale: SaleRef, page: number): ParsedSalePage | null { return res.html ? parseCataloguePage(res.html, sale, page) : null; } } export default function createConnector(meta: ConnectorMeta) { return new DunbarSloaneConnector(meta); }