← back to Wine Finder Next

lib/services/wineImageService.js

149 lines

const axios = require('axios');

/**
 * Wine Image Service - Fetches real wine bottle images
 *
 * AFFILIATE PROGRAM POLICY:
 * We can use images from retailers with affiliate programs where we are participants:
 *
 * ✅ CAN USE (Affiliate Programs Available):
 * - Vivino (Vivino Affiliates program)
 * - Wine.com (CJ Affiliate, ShareASale)
 * - Total Wine & More (Has affiliate program via Impact)
 * - K&L Wine Merchants (Affiliate program available)
 * - Wine-Searcher (Affiliate program available)
 *
 * Priority Order:
 * 1. Vivino (best wine database with real bottle photos)
 * 2. Retailer images (if affiliate partner and good quality)
 * 3. Official winery images (when available)
 */
class WineImageService {
  constructor() {
    this.vivinoApiBase = 'https://www.vivino.com/api';
    this.cache = new Map(); // Simple in-memory cache
  }

  /**
   * Get official wine bottle image from Vivino or affiliate retailers
   * Prioritizes real winery bottle photos from Vivino's database
   */
  async getWineImage(wineName, vintage = null, existingImage = null) {
    // If there's already an image from an affiliate source, keep it
    if (existingImage && this.isAffiliateImage(existingImage)) {
      return existingImage;
    }
    try {
      // Create cache key
      const cacheKey = `${wineName}_${vintage}`;

      // Check cache first
      if (this.cache.has(cacheKey)) {
        return this.cache.get(cacheKey);
      }

      // Search Vivino for the wine to get its official bottle image
      const searchParams = new URLSearchParams({
        'q': wineName,
        'country_code': 'US',
        'currency_code': 'USD',
        'wine_type_ids[]': '1'  // All wine types
      });

      const response = await axios.get(
        `${this.vivinoApiBase}/explore/explore?${searchParams.toString()}`,
        {
          headers: {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Accept': 'application/json'
          },
          timeout: 5000
        }
      );

      // Find matching wine
      if (response.data.explore_vintage?.matches?.length > 0) {
        const matches = response.data.explore_vintage.matches;

        // Try to find exact vintage match first
        let match = matches.find(m =>
          m.vintage?.year === parseInt(vintage) &&
          m.vintage?.wine?.name?.toLowerCase().includes(wineName.toLowerCase())
        );

        // If no exact match, take first result
        if (!match) {
          match = matches[0];
        }

        const wine = match.vintage?.wine;

        if (wine?.image?.location) {
          // Vivino bottle images - these are real winery photos!
          const imageUrl = `https://images.vivino.com/thumbs/${wine.image.location}`;

          // Cache the result
          this.cache.set(cacheKey, imageUrl);

          return imageUrl;
        }
      }

      return null;
    } catch (error) {
      console.error('Wine image service error:', error.message);
      return null;
    }
  }

  /**
   * Check if image is from an affiliate partner (legal to use)
   */
  isAffiliateImage(imageUrl) {
    if (!imageUrl) return false;

    const affiliatePartners = [
      'images.vivino.com',         // Vivino Affiliates
      'wine.com',                  // Wine.com affiliate program
      'totalwine.com',             // Total Wine & More affiliate program
      'klwines.com',               // K&L Wine Merchants affiliate program
      'wine-searcher.com',         // Wine-Searcher affiliate program
    ];

    const urlLower = imageUrl.toLowerCase();
    return affiliatePartners.some(partner => urlLower.includes(partner));
  }

  /**
   * Validate if an image URL is from a trusted source (affiliate or winery)
   * Accepts both affiliate partners and official winery images
   */
  isOfficialWineImage(imageUrl) {
    if (!imageUrl) return false;

    // Accept affiliate partners
    if (this.isAffiliateImage(imageUrl)) {
      return true;
    }

    // Also accept official winery domains
    const wineryDomains = [
      'cellartracker.com',         // CellarTracker database
      'winery.com',                // Winery direct
      'vineyard.com',              // Vineyard direct
    ];

    const urlLower = imageUrl.toLowerCase();
    return wineryDomains.some(domain => urlLower.includes(domain));
  }

  /**
   * Clear cache (useful for testing or memory management)
   */
  clearCache() {
    this.cache.clear();
  }
}

module.exports = new WineImageService();