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