← back to Wine Finder Next
scripts/smart-wine-tracker.js
184 lines
#!/usr/bin/env node
/**
* SMART WINE PRICE TRACKER
* Sustainable rate-limited tracking
* Tracks 10 wines every 5 minutes (rotating through library)
*/
const axios = require('axios');
const fs = require('fs').promises;
const path = require('path');
const DATA_DIR = '/root/Projects/wine-finder-next/data';
const PRICE_HISTORY_FILE = path.join(DATA_DIR, 'price-history.jsonl');
const FLASH_DEALS_FILE = path.join(DATA_DIR, 'flash-deals.json');
const API_URL = 'http://localhost:7201/api/search';
// SMART ROTATION: Track 10 wines per cycle, rotate through library
const WINE_LIBRARY = [
// Popular wines that people actually search for
'cabernet sauvignon', 'pinot noir', 'chardonnay', 'merlot', 'zinfandel',
'sauvignon blanc', 'malbec', 'syrah', 'riesling', 'prosecco',
// Premium brands (but realistic)
'caymus', 'silver oak', 'opus one', 'stags leap', 'duckhorn',
'far niente', 'shafer', 'ridge', 'joseph phelps', 'heitz',
// Popular regions
'napa valley', 'sonoma', 'paso robles', 'bordeaux', 'burgundy',
'tuscany', 'barolo', 'brunello', 'rioja', 'champagne'
];
let previousPrices = new Map();
let runCount = 0;
let currentBatchIndex = 0;
async function crawlBatch() {
const allWines = [];
// Get next batch of 10 wines (rotate through library)
const batchSize = 10;
const batch = [];
for (let i = 0; i < batchSize; i++) {
const index = (currentBatchIndex + i) % WINE_LIBRARY.length;
batch.push(WINE_LIBRARY[index]);
}
// Move to next batch for next run
currentBatchIndex = (currentBatchIndex + batchSize) % WINE_LIBRARY.length;
console.log(` Searching: ${batch.join(', ')}`);
// Search ONE wine at a time with delays to avoid rate limiting
for (const query of batch) {
try {
const response = await axios.get(API_URL, {
params: {
q: query,
sources: 'vivino', // Only Vivino for now, others are blocked
limit: 10
},
timeout: 15000
});
const wines = response.data.wines || [];
allWines.push(...wines);
// Wait 2 seconds between requests to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (err) {
console.error(` Error searching ${query}:`, err.message);
// Wait longer on error
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
return allWines;
}
async function detectFlashDeals(wines) {
const flashDeals = [];
const now = new Date().toISOString();
for (const wine of wines) {
const key = `${wine.name}|${wine.source}`;
const prev = previousPrices.get(key);
if (prev && prev.price > wine.price) {
const discount = ((prev.price - wine.price) / prev.price * 100).toFixed(1);
flashDeals.push({
...wine,
wasPrice: prev.price,
nowPrice: wine.price,
discount: parseFloat(discount),
detectedAt: now,
secondsAgo: Math.floor((Date.now() - prev.timestamp) / 1000)
});
}
previousPrices.set(key, { price: wine.price, timestamp: Date.now() });
}
return flashDeals;
}
async function logData(wines, flashDeals) {
const timestamp = new Date().toISOString();
// Log prices
const entries = wines.map(w => ({ timestamp, ...w }));
const lines = entries.map(e => JSON.stringify(e)).join('\n') + '\n';
await fs.appendFile(PRICE_HISTORY_FILE, lines);
// Save flash deals
if (flashDeals.length > 0) {
await fs.writeFile(FLASH_DEALS_FILE, JSON.stringify({
timestamp,
count: flashDeals.length,
deals: flashDeals
}, null, 2));
}
}
async function runContinuous() {
console.log('🍷 SMART WINE PRICE TRACKER');
console.log('Wine Library:', WINE_LIBRARY.length, 'wines');
console.log('Batch Size: 10 wines per cycle');
console.log('Frequency: Every 5 minutes');
console.log('Rate Limit: 2 seconds between requests');
console.log('Started:', new Date().toISOString());
console.log('Running...\n');
await fs.mkdir(DATA_DIR, { recursive: true });
while (true) {
runCount++;
const startTime = Date.now();
try {
console.log(`\n[Cycle ${runCount}] ${new Date().toLocaleTimeString()}`);
const wines = await crawlBatch();
const flashDeals = await detectFlashDeals(wines);
await logData(wines, flashDeals);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
const status = flashDeals.length > 0 ? `🔥 ${flashDeals.length} FLASH DEALS` : '✓';
console.log(` Result: ${wines.length} wines found in ${elapsed}s ${status}`);
if (flashDeals.length > 0) {
flashDeals.forEach(d => {
console.log(` 🔥 ${d.name}: ${d.discount}% OFF - was $${d.wasPrice} now $${d.nowPrice}`);
});
}
} catch (err) {
console.error(`[Cycle ${runCount}] Error:`, err.message);
}
// Wait 5 minutes (300 seconds)
console.log(` Next cycle in 5 minutes...\n`);
await new Promise(resolve => setTimeout(resolve, 300000));
}
}
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\n\nShutting down gracefully...');
console.log(`Total cycles completed: ${runCount}`);
console.log(`Wines in rotation: ${currentBatchIndex}/${WINE_LIBRARY.length}`);
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n\nShutting down gracefully...');
console.log(`Total cycles completed: ${runCount}`);
console.log(`Wines in rotation: ${currentBatchIndex}/${WINE_LIBRARY.length}`);
process.exit(0);
});
runContinuous().catch(console.error);