← back to Wine Finder

services/wineEnthusiastScraper.js

167 lines

const fs = require('fs');
const path = require('path');
const csv = require('csv-parser');

class WineEnthusiastScraper {
  constructor() {
    this.dataPath = path.join(__dirname, '../data/winemag-data-130k-v2.csv');
    this.cache = null;
    this.cacheTimestamp = null;
    this.CACHE_TTL = 3600000; // 1 hour
  }

  /**
   * Search Wine Enthusiast 130k dataset
   * @param {string} query - Search query
   * @param {object} options - Search options
   */
  async search(query, options = {}) {
    try {
      const {
        limit = 50,
        minRating = 0,
        maxPrice = 10000,
        country = null,
        variety = null
      } = options;

      // Load wines from CSV
      const wines = await this.loadWines();

      // Filter by query
      const queryLower = query.toLowerCase();
      let filtered = wines.filter(wine => {
        const title = (wine.title || '').toLowerCase();
        const description = (wine.description || '').toLowerCase();
        const winery = (wine.winery || '').toLowerCase();
        const variety = (wine.variety || '').toLowerCase();

        return title.includes(queryLower) ||
               description.includes(queryLower) ||
               winery.includes(queryLower) ||
               variety.includes(queryLower);
      });

      // Apply filters
      filtered = filtered.filter(wine => {
        const price = parseFloat(wine.price) || 0;
        const points = parseInt(wine.points) || 0;

        let matches = points >= minRating && (price === 0 || price <= maxPrice);

        if (country && wine.country) {
          matches = matches && wine.country.toLowerCase().includes(country.toLowerCase());
        }

        if (variety && wine.variety) {
          matches = matches && wine.variety.toLowerCase().includes(variety.toLowerCase());
        }

        return matches;
      });

      // Sort by points (rating) descending
      filtered.sort((a, b) => (parseInt(b.points) || 0) - (parseInt(a.points) || 0));

      // Limit results
      filtered = filtered.slice(0, limit);

      // Format wines
      const formattedWines = filtered.map(wine => ({
        name: wine.title || 'Unknown',
        vintage: this.extractVintage(wine.title),
        price: wine.price ? `$${wine.price}` : null,
        rating: parseInt(wine.points) || null,
        country: wine.country || '',
        region: wine.region_1 || wine.province || '',
        winery: wine.winery || '',
        variety: wine.variety || '',
        description: wine.description || '',
        designation: wine.designation || '',
        url: `https://www.winemag.com/?s=${encodeURIComponent(wine.title)}`,
        availability: 'Wine Enthusiast Database',
        source: 'Wine Enthusiast',
        type: wine.variety || 'Wine'
      }));

      return {
        success: true,
        wines: formattedWines,
        count: formattedWines.length,
        source: 'Wine Enthusiast',
        datasetInfo: '130k professional wine reviews from Wine Enthusiast Magazine',
        timestamp: new Date().toISOString()
      };

    } catch (error) {
      console.error('Wine Enthusiast scraper error:', error.message);
      return {
        success: false,
        wines: [],
        error: error.message,
        source: 'Wine Enthusiast'
      };
    }
  }

  /**
   * Load wines from CSV file
   */
  async loadWines() {
    // Return cached data if available and not expired
    if (this.cache && this.cacheTimestamp && (Date.now() - this.cacheTimestamp) < this.CACHE_TTL) {
      return this.cache;
    }

    return new Promise((resolve, reject) => {
      const wines = [];

      if (!fs.existsSync(this.dataPath)) {
        return resolve([]);
      }

      fs.createReadStream(this.dataPath)
        .pipe(csv())
        .on('data', (row) => {
          wines.push(row);
        })
        .on('end', () => {
          this.cache = wines;
          this.cacheTimestamp = Date.now();
          console.log(`Loaded ${wines.length} wines from Wine Enthusiast dataset`);
          resolve(wines);
        })
        .on('error', (error) => {
          console.error('CSV parsing error:', error);
          reject(error);
        });
    });
  }

  /**
   * Extract vintage year from wine title
   */
  extractVintage(title) {
    if (!title) return '';
    const match = title.match(/\b(19|20)\d{2}\b/);
    return match ? match[0] : '';
  }

  /**
   * Get statistics about the dataset
   */
  async getStats() {
    const wines = await this.loadWines();

    return {
      totalWines: wines.length,
      countries: [...new Set(wines.map(w => w.country).filter(c => c))].length,
      varieties: [...new Set(wines.map(w => w.variety).filter(v => v))].length,
      avgPrice: wines.reduce((sum, w) => sum + (parseFloat(w.price) || 0), 0) / wines.length,
      avgRating: wines.reduce((sum, w) => sum + (parseInt(w.points) || 0), 0) / wines.length
    };
  }
}

module.exports = new WineEnthusiastScraper();