← back to Wine Finder Next
lib/services/newItemsTracker.js
119 lines
/**
* New Items Tracker
*
* Tracks which wines have been seen before and identifies NEW listings
* by comparing current crawl against historical database
*/
const fs = require('fs').promises;
const path = require('path');
class NewItemsTracker {
constructor() {
this.dbPath = path.join(__dirname, '..', 'data', 'seen_wines.json');
this.seenWines = new Map();
this.newWinesFound = [];
}
async load() {
try {
const data = await fs.readFile(this.dbPath, 'utf8');
const parsed = JSON.parse(data);
this.seenWines = new Map(Object.entries(parsed.wines || {}));
console.log(`✓ Loaded ${this.seenWines.size} previously seen wines`);
} catch (error) {
// File doesn't exist yet - first run
console.log('⚠ No previous wine history found - all wines will be marked as NEW');
}
}
async save() {
const data = {
lastUpdated: new Date().toISOString(),
totalWines: this.seenWines.size,
wines: Object.fromEntries(this.seenWines)
};
await fs.writeFile(this.dbPath, JSON.stringify(data, null, 2));
console.log(`✓ Saved ${this.seenWines.size} wines to history`);
}
// Generate unique key for a wine
generateKey(wine) {
const name = (wine.name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
const vintage = wine.vintage || '';
const source = wine.source || '';
return `${name}_${vintage}_${source}`.substring(0, 100);
}
// Check if wine is new (not seen before)
isNew(wine) {
const key = this.generateKey(wine);
return !this.seenWines.has(key);
}
// Mark wine as seen
markAsSeen(wine) {
const key = this.generateKey(wine);
this.seenWines.set(key, {
firstSeen: new Date().toISOString(),
name: wine.name,
source: wine.source,
price: wine.price
});
}
// Process wines and identify new ones
async processWines(wines) {
await this.load();
this.newWinesFound = [];
for (const wine of wines) {
if (this.isNew(wine)) {
wine.isNew = true;
wine.newItemDate = new Date().toISOString();
this.newWinesFound.push(wine);
this.markAsSeen(wine);
} else {
wine.isNew = false;
}
}
await this.save();
console.log(`\n🆕 NEW WINES FOUND: ${this.newWinesFound.length} out of ${wines.length} total`);
return {
allWines: wines,
newWines: this.newWinesFound,
newCount: this.newWinesFound.length,
totalCount: wines.length
};
}
// Get summary of new items
getNewItemsSummary() {
if (this.newWinesFound.length === 0) {
return 'No new wines found in this crawl.';
}
const bySource = {};
this.newWinesFound.forEach(wine => {
if (!bySource[wine.source]) {
bySource[wine.source] = 0;
}
bySource[wine.source]++;
});
let summary = `Found ${this.newWinesFound.length} NEW wines:\n`;
Object.keys(bySource).sort().forEach(source => {
summary += ` - ${source}: ${bySource[source]} new\n`;
});
return summary;
}
}
module.exports = NewItemsTracker;