import { Page } from 'playwright'; import { BaseScraper } from './base-scraper'; import { VendorListing } from './types'; export class EldoradoScraper extends BaseScraper { getVendorName(): 'eldorado' { return 'eldorado'; } getTargetUrl(): string { return 'https://www.eldorado.gg/star-citizen-auec/g/141-0-0'; } async extractListings(page: Page): Promise { // Wait for page readiness await page.waitForSelector('text=Star Citizen aUEC', { timeout: 15000 }).catch(() => {}); // Wait for price elements to appear await page.waitForTimeout(3000); const listings = await page.evaluate(() => { const results: Array<{ amountAUEC: number; priceUSD: number; pricePerMillion: number; seller?: string; deliveryTime?: string; }> = []; const bodyText = document.body.innerText; const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l.length > 0); // Track seen combinations to avoid duplicates const seenListings = new Set(); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Look for "$/M" pattern - this is the direct price per million // Examples: "$0.00007 / M", "$0.00018 / M", "$0.00007/M" const pricePerMMatch = line.match(/\$\s*([\d.]+)\s*\/?\s*\/\s*M/i) || line.match(/\$\s*([\d.]+)\s*\/\s*M/i); if (pricePerMMatch) { const pricePerMillion = parseFloat(pricePerMMatch[1]); // Look for "Min. qty" or "Min qty" nearby to get the quantity let minQtyM = 10000; // Default to 10000M for (let j = Math.max(0, i - 5); j < Math.min(lines.length, i + 5); j++) { const qtyLine = lines[j]; // Match patterns like "Min. qty. 6000 M" or "Min qty: 16,000 M" const qtyMatch = qtyLine.match(/Min\.?\s*qty\.?\s*:?\s*([\d,]+)\s*M/i); if (qtyMatch) { minQtyM = parseFloat(qtyMatch[1].replace(/,/g, '')); break; } } const amountAUEC = minQtyM * 1_000_000; const priceUSD = pricePerMillion * minQtyM; // Find seller name - look both backwards and forwards // For featured seller, name appears BEFORE the price // For other sellers, name appears in a structured list let seller: string | undefined; // Search backwards first (for featured seller and some list items) for (let j = Math.max(0, i - 20); j < i; j++) { const sellerLine = lines[j]; // Skip common non-seller text if ( sellerLine.includes('$') || sellerLine.includes('Price') || sellerLine.includes('qty') || sellerLine.includes('stock') || sellerLine.includes('Delivery') || sellerLine.toLowerCase().includes('review') || sellerLine.includes('Rating') || sellerLine.includes('Offer') || sellerLine.includes('Details') || sellerLine.includes('FEATURED') || sellerLine.includes('Other') || sellerLine.includes('sellers') || sellerLine.includes('aUEC') || sellerLine === 'Star Citizen' || // Exclude the game title but not seller names sellerLine.includes('IMF') || sellerLine.includes('in-game') || sellerLine.includes('currency') || sellerLine.length < 3 || sellerLine.length > 30 ) { continue; } // Match seller name patterns - alphanumeric with underscores/hyphens // Allow some special cases like "StarCitizen" if (/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(sellerLine)) { seller = sellerLine; // Don't break - keep looking for a closer match } } // Find delivery time let deliveryTime: string | undefined; for (let j = Math.max(0, i - 5); j < Math.min(lines.length, i + 5); j++) { const deliveryLine = lines[j]; if ( deliveryLine.match(/\d+\s*min/i) || deliveryLine.match(/\d+\s*hour/i) || deliveryLine.toLowerCase().includes('instant') ) { deliveryTime = deliveryLine; break; } } // Create unique key to avoid duplicates const key = `${pricePerMillion}-${minQtyM}`; if (seenListings.has(key)) continue; seenListings.add(key); results.push({ amountAUEC, priceUSD, pricePerMillion, seller, deliveryTime, }); } } return results; }); const scrapedAt = new Date(); const url = this.getTargetUrl(); return listings.map(listing => ({ vendor: 'eldorado' as const, amountAUEC: listing.amountAUEC, priceUSD: listing.priceUSD, pricePerMillion: listing.pricePerMillion, seller: listing.seller, deliveryTime: listing.deliveryTime, scrapedAt, url, })); } }