← back to Wine Finder Next

lib/services/wineAuctionBrowser.js

321 lines

/**
 * WINE AUCTION BROWSER SCRAPER
 * Uses Puppeteer to bypass anti-bot protection
 * Tracks $20-$10,000+ wines from ALL major auction sites
 */

const puppeteer = require('puppeteer');

class WineAuctionBrowser {
  constructor() {
    this.browser = null;
  }

  async init() {
    if (!this.browser) {
      this.browser = await puppeteer.launch({
        headless: 'new',
        args: [
          '--no-sandbox',
          '--disable-setuid-sandbox',
          '--disable-dev-shm-usage',
          '--disable-web-security',
          '--disable-features=IsolateOrigins,site-per-process'
        ]
      });
    }
    return this.browser;
  }

  async close() {
    if (this.browser) {
      await this.browser.close();
      this.browser = null;
    }
  }

  extractWinery(fullName) {
    const match = fullName.match(/^([^,0-9]+)/);
    return match ? match[1].trim() : fullName.split(' ')[0];
  }

  /**
   * WineBid.com - Largest online wine auction
   */
  async searchWineBid(query) {
    const browser = await this.init();
    const page = await browser.newPage();

    try {
      await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');

      const url = `https://www.winebid.com/BuyWine?keyword=${encodeURIComponent(query)}`;
      await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });

      const wines = await page.evaluate(() => {
        const results = [];
        document.querySelectorAll('.product-item, [data-product], .wine-listing').forEach(el => {
          const nameEl = el.querySelector('.product-name, .wine-name, h3, h4');
          const priceEl = el.querySelector('.price, .current-bid, [class*="price"]');
          const imgEl = el.querySelector('img');
          const linkEl = el.querySelector('a');

          if (nameEl && priceEl) {
            const name = nameEl.textContent.trim();
            const priceText = priceEl.textContent.trim();
            const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));

            if (name && price && price >= 20) {
              results.push({
                name,
                price,
                image: imgEl ? imgEl.src : '',
                url: linkEl ? linkEl.href : ''
              });
            }
          }
        });
        return results;
      });

      await page.close();

      return wines.map(w => ({
        ...w,
        vintage: null,
        source: 'WineBid Auction',
        availability: 'Auction Active',
        winery: this.extractWinery(w.name),
        region: '',
        rating: null,
        ratingsCount: 0,
        searchQuery: query
      }));

    } catch (error) {
      console.error('WineBid browser error:', error.message);
      await page.close();
      return [];
    }
  }

  /**
   * Zachys Wine Auctions
   */
  async searchZachys(query) {
    const browser = await this.init();
    const page = await browser.newPage();

    try {
      await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');

      const url = `https://www.zachys.com/search?q=${encodeURIComponent(query)}`;
      await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });

      await page.waitForTimeout(2000); // Wait for DataDome

      const wines = await page.evaluate(() => {
        const results = [];
        document.querySelectorAll('.product, .lot, [data-product-id]').forEach(el => {
          const nameEl = el.querySelector('.product-title, .lot-title, h2, h3');
          const priceEl = el.querySelector('.price, .estimate');
          const imgEl = el.querySelector('img');
          const linkEl = el.querySelector('a');

          if (nameEl && priceEl) {
            const name = nameEl.textContent.trim();
            const priceText = priceEl.textContent.trim();
            const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));

            if (name && price && price >= 20) {
              results.push({
                name,
                price,
                image: imgEl ? imgEl.src : '',
                url: linkEl ? linkEl.href : ''
              });
            }
          }
        });
        return results;
      });

      await page.close();

      return wines.map(w => ({
        ...w,
        vintage: null,
        source: 'Zachys Wine Auctions',
        availability: 'Auction',
        winery: this.extractWinery(w.name),
        region: '',
        rating: null,
        ratingsCount: 0,
        searchQuery: query
      }));

    } catch (error) {
      console.error('Zachys browser error:', error.message);
      await page.close();
      return [];
    }
  }

  /**
   * Acker Wines
   */
  async searchAcker(query) {
    const browser = await this.init();
    const page = await browser.newPage();

    try {
      await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');

      const url = `https://www.ackerwines.com/search?q=${encodeURIComponent(query)}`;
      await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });

      await page.waitForTimeout(2000);

      const wines = await page.evaluate(() => {
        const results = [];
        document.querySelectorAll('.product, .wine-item').forEach(el => {
          const nameEl = el.querySelector('.product-name, .wine-name, h3');
          const priceEl = el.querySelector('.price');
          const imgEl = el.querySelector('img');
          const linkEl = el.querySelector('a');

          if (nameEl && priceEl) {
            const name = nameEl.textContent.trim();
            const priceText = priceEl.textContent.trim();
            const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));

            if (name && price && price >= 20) {
              results.push({
                name,
                price,
                image: imgEl ? imgEl.src : '',
                url: linkEl ? linkEl.href : ''
              });
            }
          }
        });
        return results;
      });

      await page.close();

      return wines.map(w => ({
        ...w,
        vintage: null,
        source: 'Acker Wines',
        availability: 'Available',
        winery: this.extractWinery(w.name),
        region: '',
        rating: null,
        ratingsCount: 0,
        searchQuery: query
      }));

    } catch (error) {
      console.error('Acker browser error:', error.message);
      await page.close();
      return [];
    }
  }

  /**
   * Heritage Auctions Fine Wine
   */
  async searchHeritage(query) {
    const browser = await this.init();
    const page = await browser.newPage();

    try {
      await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');

      const url = `https://finewine.ha.com/c/search.zx?N=0&Ntt=${encodeURIComponent(query)}`;
      await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });

      const wines = await page.evaluate(() => {
        const results = [];
        document.querySelectorAll('.lot-tile, .lot').forEach(el => {
          const nameEl = el.querySelector('.lot-title, h3');
          const priceEl = el.querySelector('.current-bid, .estimate');
          const imgEl = el.querySelector('img');
          const linkEl = el.querySelector('a');

          if (nameEl && priceEl) {
            const name = nameEl.textContent.trim();
            const priceText = priceEl.textContent.trim();
            const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));

            if (name && price && price >= 20) {
              results.push({
                name,
                price,
                image: imgEl ? imgEl.src : '',
                url: linkEl ? linkEl.href : ''
              });
            }
          }
        });
        return results;
      });

      await page.close();

      return wines.map(w => ({
        ...w,
        vintage: null,
        source: 'Heritage Wine Auction',
        availability: 'Auction',
        winery: this.extractWinery(w.name),
        region: '',
        rating: null,
        ratingsCount: 0,
        searchQuery: query
      }));

    } catch (error) {
      console.error('Heritage browser error:', error.message);
      await page.close();
      return [];
    }
  }

  /**
   * Search all auction sources in parallel
   */
  async searchAll(query) {
    try {
      const results = await Promise.allSettled([
        this.searchWineBid(query),
        this.searchZachys(query),
        this.searchAcker(query),
        this.searchHeritage(query)
      ]);

      const wines = [];
      results.forEach(result => {
        if (result.status === 'fulfilled' && Array.isArray(result.value)) {
          wines.push(...result.value);
        }
      });

      // Filter $20+
      const expensiveWines = wines.filter(w => w.price >= 20);

      return {
        wines: expensiveWines,
        total: expensiveWines.length,
        sources: ['winebid', 'zachys', 'acker', 'heritage']
      };
    } catch (error) {
      console.error('Browser search error:', error.message);
      return { wines: [], total: 0, sources: [] };
    }
  }
}

module.exports = new WineAuctionBrowser();