← 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();