← back to Wine Finder Next

lib/services/wineAuctionScraper.js

352 lines

/**
 * WINE AUCTION & PREMIUM RETAILER SCRAPER
 * Tracks expensive wines from multiple sources
 * Focuses on $20+ bottles
 */

const axios = require('axios');
const cheerio = require('cheerio');

class WineAuctionScraper {
  constructor() {
    this.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
  }

  /**
   * Search The Wine House (premium retailer with $20-$10,000+ bottles)
   */
  async searchWineHouse(query) {
    try {
      const url = `https://www.winehouseliquor.com/search?q=${encodeURIComponent(query)}`;

      const response = await axios.get(url, {
        headers: {
          'User-Agent': this.userAgent,
          'Accept': 'text/html,application/xhtml+xml',
          'Accept-Language': 'en-US,en;q=0.9'
        },
        timeout: 10000
      });

      const $ = cheerio.load(response.data);
      const wines = [];

      // Parse Wine House results
      $('.product-item, .product-card, .product').each((i, el) => {
        const name = $(el).find('.product-title, .product-name, h3, h4').first().text().trim();
        const priceText = $(el).find('.price, .product-price, [class*="price"]').first().text().trim();
        const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));
        const imageUrl = $(el).find('img').first().attr('src');
        const productUrl = $(el).find('a').first().attr('href');

        if (name && price && price >= 20) {
          wines.push({
            name,
            price,
            vintage: null,
            image: imageUrl || '',
            url: productUrl && productUrl.startsWith('http') ? productUrl : `https://www.winehouseliquor.com${productUrl}`,
            source: 'The Wine House',
            availability: 'In Stock',
            winery: this.extractWinery(name),
            region: '',
            rating: null,
            ratingsCount: 0,
            searchQuery: query
          });
        }
      });

      return { wines, total: wines.length, source: 'winehouse' };
    } catch (error) {
      console.error('Wine House error:', error.message);
      return { wines: [], total: 0, source: 'winehouse', error: error.message };
    }
  }

  /**
   * Search JJ Buckley Fine Wines (high-end specialty retailer)
   */
  async searchJJBuckley(query) {
    try {
      const url = `https://www.jjbuckley.com/catalogsearch/result/?q=${encodeURIComponent(query)}`;

      const response = await axios.get(url, {
        headers: {
          'User-Agent': this.userAgent,
          'Accept': 'text/html,application/xhtml+xml',
          'Accept-Language': 'en-US,en;q=0.9'
        },
        timeout: 10000
      });

      const $ = cheerio.load(response.data);
      const wines = [];

      // Parse JJ Buckley results
      $('.product-item, .item, .product').each((i, el) => {
        const name = $(el).find('.product-name, .product-title, h2, h3').first().text().trim();
        const priceText = $(el).find('.price, [data-price-type="finalPrice"]').first().text().trim();
        const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));
        const imageUrl = $(el).find('img').first().attr('src');
        const productUrl = $(el).find('a').first().attr('href');

        if (name && price && price >= 20) {
          wines.push({
            name,
            price,
            vintage: null,
            image: imageUrl || '',
            url: productUrl || '',
            source: 'JJ Buckley Fine Wines',
            availability: 'Available',
            winery: this.extractWinery(name),
            region: '',
            rating: null,
            ratingsCount: 0,
            searchQuery: query
          });
        }
      });

      return { wines, total: wines.length, source: 'jjbuckley' };
    } catch (error) {
      console.error('JJ Buckley error:', error.message);
      return { wines: [], total: 0, source: 'jjbuckley', error: error.message };
    }
  }

  /**
   * Search WineBid.com (largest online wine auction)
   */
  async searchWineBid(query) {
    try {
      const url = `https://www.winebid.com/BuyWine?keyword=${encodeURIComponent(query)}`;

      const response = await axios.get(url, {
        headers: { 'User-Agent': this.userAgent },
        timeout: 10000
      });

      const $ = cheerio.load(response.data);
      const wines = [];

      // Parse auction listings
      $('.auction-item').each((i, el) => {
        const name = $(el).find('.wine-name').text().trim();
        const priceText = $(el).find('.current-bid').text().trim();
        const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));
        const vintage = $(el).find('.vintage').text().trim();
        const imageUrl = $(el).find('img').attr('src');
        const auctionUrl = 'https://www.winebid.com' + $(el).find('a').attr('href');

        if (name && price) {
          wines.push({
            name,
            price,
            vintage: parseInt(vintage) || null,
            image: imageUrl || '',
            url: auctionUrl,
            source: 'WineBid Auction',
            availability: 'Auction Active',
            winery: this.extractWinery(name),
            region: '',
            rating: null,
            ratingsCount: 0,
            searchQuery: query
          });
        }
      });

      return { wines, total: wines.length, source: 'winebid' };
    } catch (error) {
      console.error('WineBid scrape error:', error.message);
      return { wines: [], total: 0, source: 'winebid', error: error.message };
    }
  }

  /**
   * Search K&L Wine Auctions (premium selections)
   */
  async searchKLAuctions(query) {
    try {
      // K&L redirects auction traffic to Heritage Auctions
      const url = `https://finewine.ha.com/c/search.zx?N=0&Ntt=${encodeURIComponent(query)}`;

      const response = await axios.get(url, {
        headers: { 'User-Agent': this.userAgent },
        timeout: 10000
      });

      const $ = cheerio.load(response.data);
      const wines = [];

      // Parse Heritage auction results
      $('.lot-tile').each((i, el) => {
        const name = $(el).find('.lot-title').text().trim();
        const priceText = $(el).find('.current-bid, .estimate').text().trim();
        const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));
        const imageUrl = $(el).find('img').attr('src');
        const lotUrl = $(el).find('a').attr('href');

        if (name && price) {
          wines.push({
            name,
            price,
            vintage: null,
            image: imageUrl || '',
            url: lotUrl,
            source: 'Heritage Wine Auction',
            availability: 'Auction',
            winery: this.extractWinery(name),
            region: '',
            rating: null,
            ratingsCount: 0,
            searchQuery: query
          });
        }
      });

      return { wines, total: wines.length, source: 'heritage' };
    } catch (error) {
      console.error('Heritage auction error:', error.message);
      return { wines: [], total: 0, source: 'heritage', error: error.message };
    }
  }

  /**
   * Search Zachys Wine Auctions (premium rare wines)
   */
  async searchZachys(query) {
    try {
      const url = `https://www.zachys.com/search?q=${encodeURIComponent(query)}`;

      const response = await axios.get(url, {
        headers: { 'User-Agent': this.userAgent },
        timeout: 10000
      });

      const $ = cheerio.load(response.data);
      const wines = [];

      // Parse Zachys auction results
      $('.product-item, .lot-item').each((i, el) => {
        const name = $(el).find('.product-name, .lot-name').text().trim();
        const priceText = $(el).find('.price, .estimate').text().trim();
        const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));
        const imageUrl = $(el).find('img').attr('src');
        const lotUrl = $(el).find('a').attr('href');

        if (name && price && price >= 20) {
          wines.push({
            name,
            price,
            vintage: null,
            image: imageUrl ? `https://www.zachys.com${imageUrl}` : '',
            url: lotUrl ? `https://www.zachys.com${lotUrl}` : '',
            source: 'Zachys Wine Auctions',
            availability: 'Auction',
            winery: this.extractWinery(name),
            region: '',
            rating: null,
            ratingsCount: 0,
            searchQuery: query
          });
        }
      });

      return { wines, total: wines.length, source: 'zachys' };
    } catch (error) {
      console.error('Zachys auction error:', error.message);
      return { wines: [], total: 0, source: 'zachys', error: error.message };
    }
  }

  /**
   * Search Acker Wines (world's largest rare wine auction house)
   */
  async searchAcker(query) {
    try {
      const url = `https://www.ackerwines.com/search?q=${encodeURIComponent(query)}`;

      const response = await axios.get(url, {
        headers: { 'User-Agent': this.userAgent },
        timeout: 10000
      });

      const $ = cheerio.load(response.data);
      const wines = [];

      // Parse Acker auction results
      $('.wine-item, .product').each((i, el) => {
        const name = $(el).find('.wine-name, .product-title').text().trim();
        const priceText = $(el).find('.price, .current-bid').text().trim();
        const price = parseFloat(priceText.replace(/[^0-9.]/g, ''));
        const imageUrl = $(el).find('img').attr('src');
        const lotUrl = $(el).find('a').attr('href');

        if (name && price && price >= 20) {
          wines.push({
            name,
            price,
            vintage: null,
            image: imageUrl || '',
            url: lotUrl ? `https://www.ackerwines.com${lotUrl}` : '',
            source: 'Acker Wines',
            availability: 'Available',
            winery: this.extractWinery(name),
            region: '',
            rating: null,
            ratingsCount: 0,
            searchQuery: query
          });
        }
      });

      return { wines, total: wines.length, source: 'acker' };
    } catch (error) {
      console.error('Acker auction error:', error.message);
      return { wines: [], total: 0, source: 'acker', error: error.message };
    }
  }

  /**
   * Aggregate search across all sources
   * Focuses on premium wine retailers with $20+ inventory
   */
  async searchAll(query) {
    const results = await Promise.allSettled([
      this.searchWineHouse(query),
      this.searchJJBuckley(query)
      // Note: Wine auction sites (WineBid, Zachys, Acker, Wine-Searcher) all block scraping
    ]);

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

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

    return {
      wines: expensiveWines,
      total: expensiveWines.length,
      sources: ['winehouse', 'jjbuckley']
    };
  }

  /**
   * Extract winery name from full wine name
   */
  extractWinery(fullName) {
    // Try to extract first part before year or comma
    const match = fullName.match(/^([^,0-9]+)/);
    return match ? match[1].trim() : fullName.split(' ')[0];
  }
}

module.exports = new WineAuctionScraper();