package auctiora; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.sql.SQLException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * Monitoring service for Troostwijk auction lots. * This class focuses on: * - Monitoring bid changes on lots (populated by external scraper) * - Sending notifications for important events * - Coordinating image processing * * Does NOT handle scraping - that's done by the external ARCHITECTURE-TROOSTWIJK-SCRAPER process. */ public class TroostwijkMonitor { private static final String LOT_API = "https://api.troostwijkauctions.com/lot/7/list"; private final RateLimitedHttpClient httpClient; private final ObjectMapper objectMapper; public final DatabaseService db; private final NotificationService notifier; private final ObjectDetectionService detector; private final ImageProcessingService imageProcessor; /** * Constructor for the monitoring service. * * @param databasePath Path to SQLite database file (shared with external scraper) * @param notificationConfig "desktop" or "smtp:user:pass:email" * @param yoloCfgPath YOLO config file path * @param yoloWeightsPath YOLO weights file path * @param classNamesPath Class names file path */ public TroostwijkMonitor(String databasePath, String notificationConfig, String yoloCfgPath, String yoloWeightsPath, String classNamesPath) throws SQLException, IOException { this.httpClient = new RateLimitedHttpClient(); this.objectMapper = new ObjectMapper(); this.db = new DatabaseService(databasePath); this.notifier = new NotificationService(notificationConfig, ""); this.detector = new ObjectDetectionService(yoloCfgPath, yoloWeightsPath, classNamesPath); this.imageProcessor = new ImageProcessingService(db, detector, httpClient); // Initialize database schema db.ensureSchema(); } /** * Schedules periodic monitoring of all lots. * Runs every hour to refresh bids and detect changes. * Increases frequency for lots closing soon. */ public void scheduleMonitoring() { var scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() -> { try { var activeLots = db.getActiveLots(); Console.println("Monitoring " + activeLots.size() + " active lots..."); for (var lot : activeLots) { // Refresh lot bidding information refreshLotBid(lot); // Check closing time var minutesLeft = lot.minutesUntilClose(); if (minutesLeft < 30) { // Send warning when within 5 minutes if (minutesLeft <= 5 && !lot.closingNotified()) { notifier.sendNotification( "Kavel " + lot.lotId() + " sluit binnen " + minutesLeft + " min.", "Lot nearing closure", 1); // Update notification flag var updated = new Lot( lot.saleId(), lot.lotId(), lot.title(), lot.description(), lot.manufacturer(), lot.type(), lot.year(), lot.category(), lot.currentBid(), lot.currency(), lot.url(), lot.closingTime(), true ); db.updateLotNotificationFlags(updated); } // Schedule additional quick check scheduler.schedule(() -> refreshLotBid(lot), 5, TimeUnit.MINUTES); } } } catch (SQLException e) { System.err.println("Error during scheduled monitoring: " + e.getMessage()); } }, 0, 1, TimeUnit.HOURS); Console.println("✓ Monitoring service started"); } /** * Refreshes the bid for a single lot and sends notification if changed. * * @param lot the lot to refresh */ private void refreshLotBid(Lot lot) { try { var url = LOT_API + "?batchSize=1&listType=7&offset=0&sortOption=0&saleID=" + lot.saleId() + "&parentID=0&relationID=0&buildversion=201807311&lotID=" + lot.lotId(); var response = httpClient.sendGet(url); if (response.statusCode() != 200) return; var root = objectMapper.readTree(response.body()); var results = root.path("results"); if (results.isArray() && !results.isEmpty()) { var node = results.get(0); var newBid = node.path("cb").asDouble(); if (Double.compare(newBid, lot.currentBid()) > 0) { var previous = lot.currentBid(); // Create updated lot with new bid var updatedLot = new Lot( lot.saleId(), lot.lotId(), lot.title(), lot.description(), lot.manufacturer(), lot.type(), lot.year(), lot.category(), newBid, lot.currency(), lot.url(), lot.closingTime(), lot.closingNotified() ); db.updateLotCurrentBid(updatedLot); var msg = String.format("Nieuw bod op kavel %d: €%.2f (was €%.2f)", lot.lotId(), newBid, previous); notifier.sendNotification(msg, "Kavel bieding update", 0); } } } catch (IOException | InterruptedException | SQLException e) { System.err.println("Failed to refresh bid for lot " + lot.lotId() + ": " + e.getMessage()); if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } } } /** * Prints statistics about the data in the database. */ public void printDatabaseStats() { try { var allLots = db.getAllLots(); var imageCount = db.getImageCount(); Console.println("📊 Database Summary:"); Console.println(" Total lots in database: " + allLots.size()); Console.println(" Total images processed: " + imageCount); if (!allLots.isEmpty()) { var totalBids = allLots.stream().mapToDouble(Lot::currentBid).sum(); Console.println(" Total current bids: €" + String.format("%.2f", totalBids)); } } catch (SQLException e) { System.err.println(" ⚠️ Could not retrieve database stats: " + e.getMessage()); } } /** * Process pending images for lots in the database. * This should be called after the external scraper has populated lot data. */ public void processPendingImages() { imageProcessor.processPendingImages(); } }