import { z } from 'zod'; import { BaseConnector, type ConnectorMeta, type CrawlContext, type RawRecordInput, type RawRecordLike } from '@rareindex/connectors'; import { NormalizedAuctionLotSchema, NormalizedSaleSchema, SUPPORTED_CURRENCIES, type CurrencyCode, type NormalizedRecord } from '@rareindex/shared'; import { parseGradeFromTitle } from '@rareindex/taxonomy'; import { brandFromSlug, hintFromLabel, isBundleTitle, legoSetNumber, safeYear, slugFromTitle, watchReference, type DeptHint } from '../_auction-lib/categories.js'; /** * Sotheby's — results (price incl. premium) and upcoming lots from the public auction pages and the * page's own GraphQL lot-card pagination. Engine: plain HTTPS (+ Firecrawl for link discovery). */ const SITE = 'https://www.sothebys.com'; const GRAPHQL = 'https://clientapi.prod.sothelabs.com/graphql'; export const AuctionSchema = z.object({ auctionId: z.string(), url: z.string(), title: z.string(), saleNumber: z.string().nullable(), state: z.string().nullable(), // Opened | Closed | Published… type: z.string().nullable(), // Timed | Live departments: z.array(z.string()), currency: z.string().nullable(), location: z.string().nullable(), startsAt: z.string().nullable(), endsAt: z.string().nullable(), closedAt: z.string().nullable(), totalLots: z.number().nullable(), }); export type Auction = z.infer; export const LotSchema = z.object({ lotId: z.string(), lotNumber: z.string().nullable(), title: z.string(), creators: z.string().nullable(), slug: z.string().nullable(), estimateLow: z.number().nullable(), estimateHigh: z.number().nullable(), isClosed: z.boolean().nullable(), closingTime: z.string().nullable(), currentBid: z.number().nullable(), bidCurrency: z.string().nullable(), isSold: z.boolean().nullable(), finalPrice: z.number().nullable(), finalCurrency: z.string().nullable(), numberOfBids: z.number().nullable(), imageUrl: z.string().nullable(), withdrawn: z.boolean(), }); export type Lot = z.infer; export const PagePayloadSchema = z.object({ kind: z.literal('auction_page'), auction: AuctionSchema, offset: z.number().int(), lots: z.array(LotSchema), }); export type PagePayload = z.infer; const ConfigSchema = z.object({ seeds: z.array(z.string()).default([]), discoveryPages: z.array(z.string()).default([]), departments: z.array(z.string()).default([]), maxAuctionsPerRun: z.number().int().default(25), pageSize: z.number().int().min(1).max(48).default(48), }); const LOT_QUERY = `query LotCardsFilterByPaginated($id: String!, $limit: Int, $offset: Int) { auction(id: $id, language: ENGLISH) { id lotCards: lotCardsConnection(offset: $offset, limit: $limit, filter: ALL) { totalCount hasNextPage lots { lotId title creatorsDisplayTitle lotNumber { ... on VisibleLotNumber { lotDisplayNumber } } slug { lotSlug } withdrawnState { state } estimateV2 { ... on LowHighEstimateV2 { lowEstimate { amount } highEstimate { amount } } } bidState { isClosed closingTime numberOfBids currentBidV2 { amount currency } sold { __typename ... on ResultVisible { isSold premiums { finalPriceV2 { amount currency } } } } } media(imageSizes: [Small]) { images { renditions { url } } } } } } }`; function num(v: unknown): number | null { if (v === null || v === undefined || v === '') return null; const n = typeof v === 'number' ? v : Number.parseFloat(String(v)); return Number.isFinite(n) ? n : null; } /** Build a Lot from a GraphQL lot card or from the SSR Apollo cache (refs resolved by caller). */ function lotFrom(card: Record, bidState: Record | null): Lot { const est = card.estimateV2 ?? {}; const sold = bidState?.sold ?? {}; const fp = sold.premiums?.finalPriceV2 ?? null; const cb = bidState?.currentBidV2 ?? null; const mediaKey = Object.keys(card).find((k) => k.startsWith('media')); const img = mediaKey ? card[mediaKey]?.images?.[0]?.renditions?.[0]?.url ?? null : null; return LotSchema.parse({ lotId: String(card.lotId), lotNumber: card.lotNumber?.lotDisplayNumber ?? null, title: String(card.title ?? ''), creators: card.creatorsDisplayTitle ?? null, slug: card.slug?.lotSlug ?? null, estimateLow: num(est.lowEstimate?.amount), estimateHigh: num(est.highEstimate?.amount), isClosed: typeof bidState?.isClosed === 'boolean' ? bidState.isClosed : null, closingTime: bidState?.closingTime ?? null, currentBid: num(cb?.amount), bidCurrency: cb?.currency ?? null, isSold: typeof sold.isSold === 'boolean' ? sold.isSold : null, finalPrice: num(fp?.amount), finalCurrency: fp?.currency ?? null, numberOfBids: num(bidState?.numberOfBids), imageUrl: typeof img === 'string' ? img : null, withdrawn: card.withdrawnState?.state ? card.withdrawnState.state !== 'NotAffected' : false, }); } /** Parse an SSR auction page: auction + first lot cards from the Apollo cache. */ export function parseAuctionPage(html: string, url: string): { auction: Auction; lots: Lot[] } | null { const m = html.match(/