saleIds) {
- int foundCount = 0;
+ var foundCount = 0;
try {
- org.jsoup.nodes.Document doc = org.jsoup.Jsoup.parse(html);
+ var doc = org.jsoup.Jsoup.parse(html);
// Find all auction links (format: /a/title-A1-12345 or /a/title-A7-12345)
- org.jsoup.select.Elements auctionLinks = doc.select("a[href^='/a/']");
+ var auctionLinks = doc.select("a[href^='/a/']");
- for (org.jsoup.nodes.Element link : auctionLinks) {
- String href = link.attr("href");
+ for (var link : auctionLinks) {
+ var href = link.attr("href");
// Extract auction ID from URL
- java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("/a/.*?-A([17])-(\\d+)");
- java.util.regex.Matcher matcher = pattern.matcher(href);
+ var pattern = java.util.regex.Pattern.compile("/a/.*?-A([17])-(\\d+)");
+ var matcher = pattern.matcher(href);
if (!matcher.find()) {
continue;
}
-
- String typeNum = matcher.group(1);
- int auctionId = Integer.parseInt(matcher.group(2));
+
+ var typeNum = matcher.group(1);
+ var auctionId = Integer.parseInt(matcher.group(2));
// Skip duplicates
if (saleIds.contains(auctionId)) {
@@ -355,7 +316,7 @@ public class TroostwijkScraper {
}
// Extract auction info using JSoup
- AuctionInfo auction = extractAuctionInfo(link, href, auctionId, "A" + typeNum);
+ var auction = extractAuctionInfo(link, href, auctionId, "A" + typeNum);
// Only keep Dutch auctions
if (auction != null && "NL".equals(auction.country)) {
@@ -365,7 +326,7 @@ public class TroostwijkScraper {
// Save to database
try {
db.upsertAuction(auction);
- System.out.println(" Found Dutch auction: " + auctionId + " - " + auction.title + " (" + auction.location + ")");
+ IO.println(" Found Dutch auction: " + auctionId + " - " + auction.title + " (" + auction.location + ")");
} catch (SQLException e) {
System.err.println(" Failed to save auction: " + e.getMessage());
}
@@ -386,16 +347,16 @@ public class TroostwijkScraper {
* - Lot count (if available)
*/
private AuctionInfo extractAuctionInfo(org.jsoup.nodes.Element link, String href, int auctionId, String type) {
- AuctionInfo auction = new AuctionInfo();
+ var auction = new AuctionInfo();
auction.auctionId = auctionId;
auction.type = type;
auction.url = "https://www.troostwijkauctions.com" + href;
// Extract title from href (convert kebab-case to title)
- java.util.regex.Pattern titlePattern = java.util.regex.Pattern.compile("/a/(.+?)-A[17]-");
- java.util.regex.Matcher titleMatcher = titlePattern.matcher(href);
+ var titlePattern = java.util.regex.Pattern.compile("/a/(.+?)-A[17]-");
+ var titleMatcher = titlePattern.matcher(href);
if (titleMatcher.find()) {
- String slug = titleMatcher.group(1);
+ var slug = titleMatcher.group(1);
auction.title = slug.replace("-", " ");
// Capitalize first letter
if (!auction.title.isEmpty()) {
@@ -406,10 +367,10 @@ public class TroostwijkScraper {
}
// Try to find title in link text (more accurate)
- String linkText = link.text();
+ var linkText = link.text();
if (!linkText.isEmpty() && !linkText.matches(".*\\d+.*")) {
// If link text doesn't contain numbers, it's likely the title
- String[] parts = linkText.split(",|\\d+");
+ var parts = linkText.split(",|\\d+");
if (parts.length > 0 && parts[0].trim().length() > 5) {
auction.title = parts[0].trim();
}
@@ -417,15 +378,15 @@ public class TroostwijkScraper {
// Extract location using JSoup selectors
// Look for tags that contain location info
- org.jsoup.select.Elements locationElements = link.select("p");
- for (org.jsoup.nodes.Element p : locationElements) {
- String text = p.text();
+ var locationElements = link.select("p");
+ for (var p : locationElements) {
+ var text = p.text();
// Pattern: "City, Country" or "City, Region, Country"
if (text.matches(".*[A-Z]{2}$")) {
// Ends with 2-letter country code
- String countryCode = text.substring(text.length() - 2);
- String cityPart = text.substring(0, text.length() - 2).trim();
+ var countryCode = text.substring(text.length() - 2);
+ var cityPart = text.substring(0, text.length() - 2).trim();
// Remove trailing comma or whitespace
cityPart = cityPart.replaceAll("[,\\s]+$", "");
@@ -439,14 +400,14 @@ public class TroostwijkScraper {
// Fallback: check HTML content directly
if (auction.country == null) {
- String html = link.html();
- java.util.regex.Pattern locPattern = java.util.regex.Pattern.compile(
+ var html = link.html();
+ var locPattern = java.util.regex.Pattern.compile(
"([A-Za-z][A-Za-z\\s,\\-']+?)\\s*(?:)?\\s*\\s*([A-Z]{2})(?![A-Za-z])");
- java.util.regex.Matcher locMatcher = locPattern.matcher(html);
+ var locMatcher = locPattern.matcher(html);
if (locMatcher.find()) {
- String city = locMatcher.group(1).trim().replaceAll(",$", "");
- String country = locMatcher.group(2);
+ var city = locMatcher.group(1).trim().replaceAll(",$", "");
+ var country = locMatcher.group(2);
auction.city = city;
auction.country = country;
auction.location = city + ", " + country;
@@ -454,12 +415,12 @@ public class TroostwijkScraper {
}
// Extract lot count if available (kavels/lots)
- org.jsoup.select.Elements textElements = link.select("*");
- for (org.jsoup.nodes.Element elem : textElements) {
- String text = elem.ownText();
+ var textElements = link.select("*");
+ for (var elem : textElements) {
+ var text = elem.ownText();
if (text.matches("\\d+\\s+(?:kavel|lot|item)s?.*")) {
- java.util.regex.Pattern countPattern = java.util.regex.Pattern.compile("(\\d+)");
- java.util.regex.Matcher countMatcher = countPattern.matcher(text);
+ var countPattern = java.util.regex.Pattern.compile("(\\d+)");
+ var countMatcher = countPattern.matcher(text);
if (countMatcher.find()) {
auction.lotCount = Integer.parseInt(countMatcher.group(1));
break;
@@ -475,8 +436,8 @@ public class TroostwijkScraper {
*/
private String loadFromCache(int pageNumber) {
if (!useCache || cacheDb == null) return null;
-
- String url = pageNumber == 1
+
+ var url = pageNumber == 1
? AUCTIONS_PAGE
: AUCTIONS_PAGE + "?page=" + pageNumber;
@@ -488,8 +449,8 @@ public class TroostwijkScraper {
*/
private void saveToCache(int pageNumber, String html) {
if (!useCache || cacheDb == null) return;
-
- String url = pageNumber == 1
+
+ var url = pageNumber == 1
? AUCTIONS_PAGE
: AUCTIONS_PAGE + "?page=" + pageNumber;
@@ -506,28 +467,28 @@ public class TroostwijkScraper {
* @param saleId the sale identifier
*/
public void fetchLotsForSale(int saleId) {
- int batchSize = 200;
- int offset = 0;
- boolean more = true;
- int totalLots = 0;
+ var batchSize = 200;
+ var offset = 0;
+ var more = true;
+ var totalLots = 0;
while (more) {
try {
- String url = LOT_API + "?batchSize=" + batchSize
- + "&listType=7&offset=" + offset
- + "&sortOption=0&saleID=" + saleId
- + "&parentID=0&relationID=0&buildversion=201807311";
+ var url = LOT_API + "?batchSize=" + batchSize
+ + "&listType=7&offset=" + offset
+ + "&sortOption=0&saleID=" + saleId
+ + "&parentID=0&relationID=0&buildversion=201807311";
- System.out.println(" Fetching lots from API (offset=" + offset + ")...");
+ IO.println(" Fetching lots from API (offset=" + offset + ")...");
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .header("Accept", "application/json")
- .header("User-Agent", "Mozilla/5.0")
- .GET()
- .build();
+ var request = HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .header("Accept", "application/json")
+ .header("User-Agent", "Mozilla/5.0")
+ .GET()
+ .build();
- HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
System.err.println(" ⚠️ API call failed for sale " + saleId);
@@ -536,22 +497,22 @@ public class TroostwijkScraper {
break;
}
- JsonNode root = objectMapper.readTree(response.body());
- JsonNode results = root.path("results");
+ var root = objectMapper.readTree(response.body());
+ var results = root.path("results");
if (!results.isArray() || results.isEmpty()) {
if (offset == 0) {
- System.out.println(" ⚠️ No lots found for sale " + saleId);
- System.out.println(" API Response: " + response.body().substring(0, Math.min(500, response.body().length())));
+ IO.println(" ⚠️ No lots found for sale " + saleId);
+ IO.println(" API Response: " + response.body().substring(0, Math.min(500, response.body().length())));
}
more = false;
break;
}
- int lotsInBatch = results.size();
- System.out.println(" Found " + lotsInBatch + " lots in this batch");
+ var lotsInBatch = results.size();
+ IO.println(" Found " + lotsInBatch + " lots in this batch");
- for (JsonNode node : results) {
- Lot lot = new Lot();
+ for (var node : results) {
+ var lot = new Lot();
lot.saleId = saleId;
lot.lotId = node.path("lotID").asInt();
lot.title = node.path("t").asText();
@@ -571,26 +532,26 @@ public class TroostwijkScraper {
// Download images and perform object detection
List imageUrls = new ArrayList<>();
- JsonNode imgs = node.path("imgs");
+ var imgs = node.path("imgs");
if (imgs.isArray()) {
- for (JsonNode imgNode : imgs) {
- String imgUrl = imgNode.asText();
+ for (var imgNode : imgs) {
+ var imgUrl = imgNode.asText();
imageUrls.add(imgUrl);
}
}
// Download and analyze images (optional, can be slow)
- for (String imgUrl : imageUrls) {
- String fileName = downloadImage(imgUrl, saleId, lot.lotId);
+ for (var imgUrl : imageUrls) {
+ var fileName = downloadImage(imgUrl, saleId, lot.lotId);
if (fileName != null) {
// run object detection once per image
- List labels = detector.detectObjects(fileName);
+ var labels = detector.detectObjects(fileName);
db.insertImage(lot.lotId, imgUrl, fileName, labels);
}
}
}
- System.out.println(" ✓ Processed " + totalLots + " lots so far");
+ IO.println(" ✓ Processed " + totalLots + " lots so far");
offset += batchSize;
} catch (IOException | InterruptedException e) {
System.err.println("Error fetching lots for sale " + saleId + ": " + e.getMessage());
@@ -612,16 +573,16 @@ public class TroostwijkScraper {
*/
private String downloadImage(String imageUrl, int saleId, int lotId) {
try {
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create(imageUrl))
- .GET()
- .build();
- HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
+ var request = HttpRequest.newBuilder()
+ .uri(URI.create(imageUrl))
+ .GET()
+ .build();
+ var response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() == 200) {
- Path dir = Paths.get("images", String.valueOf(saleId), String.valueOf(lotId));
+ var dir = Paths.get("images", String.valueOf(saleId), String.valueOf(lotId));
Files.createDirectories(dir);
- String fileName = Paths.get(imageUrl).getFileName().toString();
- Path dest = dir.resolve(fileName);
+ var fileName = Paths.get(imageUrl).getFileName().toString();
+ var dest = dir.resolve(fileName);
Files.copy(response.body(), dest);
return dest.toAbsolutePath().toString();
}
@@ -639,16 +600,16 @@ public class TroostwijkScraper {
* expire, a Pushover notification is sent to the configured user.
* Note: In production, ensure proper shutdown handling for the scheduler.
*/
- public ScheduledExecutorService scheduleMonitoring() {
- ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+ public void scheduleMonitoring() {
+ var scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
try {
- List activeLots = db.getActiveLots();
- for (Lot lot : activeLots) {
+ var activeLots = db.getActiveLots();
+ for (var lot : activeLots) {
// refresh the lot's bidding information via API
refreshLotBid(lot);
// check closing time to adjust monitoring
- long minutesLeft = lot.minutesUntilClose();
+ var minutesLeft = lot.minutesUntilClose();
if (minutesLeft < 30) {
// send warning when within 5 minutes
if (minutesLeft <= 5 && !lot.closingNotified) {
@@ -665,7 +626,6 @@ public class TroostwijkScraper {
System.err.println("Error during scheduled monitoring: " + e.getMessage());
}
}, 0, 1, TimeUnit.HOURS);
- return scheduler;
}
/**
@@ -677,21 +637,21 @@ public class TroostwijkScraper {
*/
private void refreshLotBid(Lot lot) {
try {
- String url = LOT_API + "?batchSize=1&listType=7&offset=0&sortOption=0&saleID=" + lot.saleId
- + "&parentID=0&relationID=0&buildversion=201807311&lotID=" + lot.lotId;
- HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
- HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ 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 request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
+ var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) return;
- JsonNode root = objectMapper.readTree(response.body());
- JsonNode results = root.path("results");
+ var root = objectMapper.readTree(response.body());
+ var results = root.path("results");
if (results.isArray() && !results.isEmpty()) {
- JsonNode node = results.get(0);
- double newBid = node.path("cb").asDouble();
+ var node = results.get(0);
+ var newBid = node.path("cb").asDouble();
if (Double.compare(newBid, lot.currentBid) > 0) {
- double previous = lot.currentBid;
+ var previous = lot.currentBid;
lot.currentBid = newBid;
db.updateLotCurrentBid(lot);
- String msg = String.format("Nieuw bod op kavel %d: €%.2f (was €%.2f)", lot.lotId, newBid, previous);
+ var msg = String.format("Nieuw bod op kavel %d: €%.2f (was €%.2f)", lot.lotId, newBid, previous);
notifier.sendNotification(msg, "Kavel bieding update", 0);
}
}
@@ -700,78 +660,20 @@ public class TroostwijkScraper {
}
}
- /**
- * Entry point. Configure database location, notification settings, and
- * YOLO model paths here before running. Once started the scraper
- * discovers Dutch auctions, scrapes lots, and begins monitoring.
- */
- public static void main(String[] args) throws Exception {
- System.out.println("=== Troostwijk Auction Scraper ===\n");
-
- // Configuration parameters (replace with your own values)
- String databaseFile = "troostwijk.db";
-
- // Notification configuration - choose one:
- // Option 1: Desktop notifications only (free, no setup required)
- String notificationConfig = System.getenv().getOrDefault("NOTIFICATION_CONFIG", "desktop");
-
- // Option 2: Desktop + Email via Gmail (free, requires Gmail app password)
- // Format: "smtp:username:appPassword:toEmail"
- // Example: "smtp:your.email@gmail.com:abcd1234efgh5678:recipient@example.com"
- // Get app password: Google Account > Security > 2-Step Verification > App passwords
-
- // YOLO model paths (optional - scraper works without object detection)
- String yoloCfg = "models/yolov4.cfg";
- String yoloWeights = "models/yolov4.weights";
- String yoloClasses = "models/coco.names";
-
- // Load native OpenCV library
- System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
-
- System.out.println("Initializing scraper...");
- TroostwijkScraper scraper = new TroostwijkScraper(databaseFile, notificationConfig, "",
- yoloCfg, yoloWeights, yoloClasses);
-
- // Step 1: Discover auctions in NL
- System.out.println("\n[1/3] Discovering Dutch auctions...");
- List auctions = scraper.discoverDutchAuctions();
- System.out.println("✓ Found " + auctions.size() + " auctions: " + auctions);
-
- // Step 2: Fetch lots for each auction
- System.out.println("\n[2/3] Fetching lot details...");
- int totalAuctions = auctions.size();
- int currentAuction = 0;
- for (int saleId : auctions) {
- currentAuction++;
- System.out.println(" [Page " + currentAuction + "] Fetching auctions...");
- System.out.println(" [" + currentAuction + "/" + totalAuctions + "] Processing sale " + saleId + "...");
- scraper.fetchLotsForSale(saleId);
- }
-
- // Show database summary
- System.out.println("\n📊 Database Summary:");
- scraper.printDatabaseStats();
-
- // Step 3: Start monitoring bids and closures
- System.out.println("\n[3/3] Starting monitoring service...");
- scraper.scheduleMonitoring();
- System.out.println("✓ Monitoring active. Press Ctrl+C to stop.\n");
- }
-
/**
* Prints statistics about the data in the database.
*/
- private void printDatabaseStats() {
+ public void printDatabaseStats() {
try {
- List allLots = db.getAllLots();
- int imageCount = db.getImageCount();
+ var allLots = db.getAllLots();
+ var imageCount = db.getImageCount();
- System.out.println(" Total lots in database: " + allLots.size());
- System.out.println(" Total images downloaded: " + imageCount);
+ IO.println(" Total lots in database: " + allLots.size());
+ IO.println(" Total images downloaded: " + imageCount);
if (!allLots.isEmpty()) {
- double totalBids = allLots.stream().mapToDouble(l -> l.currentBid).sum();
- System.out.println(" Total current bids: €" + String.format("%.2f", totalBids));
+ var totalBids = allLots.stream().mapToDouble(l -> l.currentBid).sum();
+ IO.println(" Total current bids: €" + String.format("%.2f", totalBids));
}
} catch (SQLException e) {
System.err.println(" ⚠️ Could not retrieve database stats: " + e.getMessage());
@@ -782,610 +684,4 @@ public class TroostwijkScraper {
// Domain classes and services
// ----------------------------------------------------------------------
- /**
- * Represents auction metadata (veiling informatie)
- */
- public static class AuctionInfo {
- public int auctionId; // Unique auction ID (from URL)
- public String title; // Auction title
- public String location; // Location (e.g., "Amsterdam, NL")
- public String city; // City name
- public String country; // Country code (e.g., "NL")
- public String url; // Full auction URL
- public String type; // Auction type (A1 or A7)
- public int lotCount; // Number of lots/kavels
- public LocalDateTime closingTime; // Closing time if available
-
- @Override
- public String toString() {
- return String.format("Auction{id=%d, type=%s, title='%s', location='%s', lots=%d, url='%s'}",
- auctionId, type, title, location, lotCount, url);
- }
- }
-
- /**
- * Simple POJO representing a lot (kavel) in an auction. It keeps track
- * of the sale it belongs to, current bid and closing time. The method
- * minutesUntilClose computes how many minutes remain until the lot closes.
- */
- static class Lot {
-
- int saleId;
- int lotId;
- String title;
- String description;
- String manufacturer;
- String type;
- int year;
- String category;
- double currentBid;
- String currency;
- String url;
- LocalDateTime closingTime; // null if unknown
- boolean closingNotified;
-
- long minutesUntilClose() {
- if (closingTime == null) return Long.MAX_VALUE;
- return java.time.Duration.between(LocalDateTime.now(), closingTime).toMinutes();
- }
- }
-
- /**
- * Service for persisting auctions, lots, images, and object labels into
- * a SQLite database. Uses the Xerial JDBC driver which connects to
- * SQLite via a URL of the form "jdbc:sqlite:path_to_file"【329850066306528†L40-L63】.
- */
- static public class DatabaseService {
-
- private final String url;
- DatabaseService(String dbPath) {
- this.url = "jdbc:sqlite:" + dbPath;
- }
- /**
- * Creates tables if they do not already exist. The schema includes
- * tables for auctions, lots, images, and object labels. This method is
- * idempotent; it can be called multiple times.
- */
- void ensureSchema() throws SQLException {
- try (Connection conn = DriverManager.getConnection(url); Statement stmt = conn.createStatement()) {
- // Auctions table (veilingen)
- stmt.execute("CREATE TABLE IF NOT EXISTS auctions ("
- + "auction_id INTEGER PRIMARY KEY,"
- + "title TEXT NOT NULL,"
- + "location TEXT,"
- + "city TEXT,"
- + "country TEXT,"
- + "url TEXT NOT NULL,"
- + "type TEXT,"
- + "lot_count INTEGER DEFAULT 0,"
- + "closing_time TEXT,"
- + "discovered_at INTEGER" // Unix timestamp
- + ")");
-
- // Sales table (legacy - keep for compatibility)
- stmt.execute("CREATE TABLE IF NOT EXISTS sales ("
- + "sale_id INTEGER PRIMARY KEY,"
- + "title TEXT,"
- + "location TEXT,"
- + "closing_time TEXT"
- + ")");
-
- // Lots table
- stmt.execute("CREATE TABLE IF NOT EXISTS lots ("
- + "lot_id INTEGER PRIMARY KEY,"
- + "sale_id INTEGER,"
- + "title TEXT,"
- + "description TEXT,"
- + "manufacturer TEXT,"
- + "type TEXT,"
- + "year INTEGER,"
- + "category TEXT,"
- + "current_bid REAL,"
- + "currency TEXT,"
- + "url TEXT,"
- + "closing_time TEXT,"
- + "closing_notified INTEGER DEFAULT 0,"
- + "FOREIGN KEY (sale_id) REFERENCES auctions(auction_id)"
- + ")");
-
- // Images table
- stmt.execute("CREATE TABLE IF NOT EXISTS images ("
- + "id INTEGER PRIMARY KEY AUTOINCREMENT,"
- + "lot_id INTEGER,"
- + "url TEXT,"
- + "file_path TEXT,"
- + "labels TEXT,"
- + "FOREIGN KEY (lot_id) REFERENCES lots(lot_id)"
- + ")");
-
- // Create indexes for better query performance
- stmt.execute("CREATE INDEX IF NOT EXISTS idx_auctions_country ON auctions(country)");
- stmt.execute("CREATE INDEX IF NOT EXISTS idx_lots_sale_id ON lots(sale_id)");
- }
- }
-
- /**
- * Inserts or updates an auction record
- */
- synchronized void upsertAuction(AuctionInfo auction) throws SQLException {
- String sql = "INSERT INTO auctions (auction_id, title, location, city, country, url, type, lot_count, closing_time, discovered_at)"
- + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
- + " ON CONFLICT(auction_id) DO UPDATE SET "
- + "title = excluded.title, location = excluded.location, city = excluded.city, "
- + "country = excluded.country, url = excluded.url, type = excluded.type, "
- + "lot_count = excluded.lot_count, closing_time = excluded.closing_time";
-
- try (Connection conn = DriverManager.getConnection(url); PreparedStatement ps = conn.prepareStatement(sql)) {
- ps.setInt(1, auction.auctionId);
- ps.setString(2, auction.title);
- ps.setString(3, auction.location);
- ps.setString(4, auction.city);
- ps.setString(5, auction.country);
- ps.setString(6, auction.url);
- ps.setString(7, auction.type);
- ps.setInt(8, auction.lotCount);
- ps.setString(9, auction.closingTime != null ? auction.closingTime.toString() : null);
- ps.setLong(10, Instant.now().getEpochSecond());
- ps.executeUpdate();
- }
- }
-
- /**
- * Retrieves all auctions from the database
- */
- synchronized List getAllAuctions() throws SQLException {
- List auctions = new ArrayList<>();
- String sql = "SELECT auction_id, title, location, city, country, url, type, lot_count, closing_time FROM auctions";
-
- try (Connection conn = DriverManager.getConnection(url); Statement stmt = conn.createStatement()) {
- ResultSet rs = stmt.executeQuery(sql);
- while (rs.next()) {
- AuctionInfo auction = new AuctionInfo();
- auction.auctionId = rs.getInt("auction_id");
- auction.title = rs.getString("title");
- auction.location = rs.getString("location");
- auction.city = rs.getString("city");
- auction.country = rs.getString("country");
- auction.url = rs.getString("url");
- auction.type = rs.getString("type");
- auction.lotCount = rs.getInt("lot_count");
- String closing = rs.getString("closing_time");
- if (closing != null) {
- auction.closingTime = LocalDateTime.parse(closing);
- }
- auctions.add(auction);
- }
- }
- return auctions;
- }
-
- /**
- * Retrieves auctions by country code
- */
- synchronized List getAuctionsByCountry(String countryCode) throws SQLException {
- List auctions = new ArrayList<>();
- String sql = "SELECT auction_id, title, location, city, country, url, type, lot_count, closing_time "
- + "FROM auctions WHERE country = ?";
-
- try (Connection conn = DriverManager.getConnection(url); PreparedStatement ps = conn.prepareStatement(sql)) {
- ps.setString(1, countryCode);
- ResultSet rs = ps.executeQuery();
- while (rs.next()) {
- AuctionInfo auction = new AuctionInfo();
- auction.auctionId = rs.getInt("auction_id");
- auction.title = rs.getString("title");
- auction.location = rs.getString("location");
- auction.city = rs.getString("city");
- auction.country = rs.getString("country");
- auction.url = rs.getString("url");
- auction.type = rs.getString("type");
- auction.lotCount = rs.getInt("lot_count");
- String closing = rs.getString("closing_time");
- if (closing != null) {
- auction.closingTime = LocalDateTime.parse(closing);
- }
- auctions.add(auction);
- }
- }
- return auctions;
- }
-
- /**
- * Inserts or updates a lot record. Uses INSERT OR REPLACE to
- * implement upsert semantics so that existing rows are replaced.
- */
- synchronized void upsertLot(Lot lot) throws SQLException {
- String sql = "INSERT INTO lots (lot_id, sale_id, title, description, manufacturer, type, year, category, current_bid, currency, url, closing_time, closing_notified)"
- + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
- + " ON CONFLICT(lot_id) DO UPDATE SET "
- + "sale_id = excluded.sale_id, title = excluded.title, description = excluded.description, "
- + "manufacturer = excluded.manufacturer, type = excluded.type, year = excluded.year, category = excluded.category, "
- + "current_bid = excluded.current_bid, currency = excluded.currency, url = excluded.url, closing_time = excluded.closing_time";
- try (Connection conn = DriverManager.getConnection(url); PreparedStatement ps = conn.prepareStatement(sql)) {
- ps.setInt(1, lot.lotId);
- ps.setInt(2, lot.saleId);
- ps.setString(3, lot.title);
- ps.setString(4, lot.description);
- ps.setString(5, lot.manufacturer);
- ps.setString(6, lot.type);
- ps.setInt(7, lot.year);
- ps.setString(8, lot.category);
- ps.setDouble(9, lot.currentBid);
- ps.setString(10, lot.currency);
- ps.setString(11, lot.url);
- ps.setString(12, lot.closingTime != null ? lot.closingTime.toString() : null);
- ps.setInt(13, lot.closingNotified ? 1 : 0);
- ps.executeUpdate();
- }
- }
-
- /**
- * Inserts a new image record. Each image is associated with a lot and
- * stores both the original URL and the local file path. Detected
- * labels are stored as a comma separated string.
- */
- synchronized void insertImage(int lotId, String url, String filePath, List labels) throws SQLException {
- String sql = "INSERT INTO images (lot_id, url, file_path, labels) VALUES (?, ?, ?, ?)";
- try (Connection conn = DriverManager.getConnection(this.url); PreparedStatement ps = conn.prepareStatement(sql)) {
- ps.setInt(1, lotId);
- ps.setString(2, url);
- ps.setString(3, filePath);
- ps.setString(4, String.join(",", labels));
- ps.executeUpdate();
- }
- }
-
- /**
- * Retrieves all lots that are still active (i.e., have a closing time
- * in the future or unknown). Only these lots need to be monitored.
- */
- synchronized List getActiveLots() throws SQLException {
- List list = new ArrayList<>();
- String sql = "SELECT lot_id, sale_id, current_bid, currency, closing_time, closing_notified FROM lots";
- try (Connection conn = DriverManager.getConnection(url); Statement stmt = conn.createStatement()) {
- ResultSet rs = stmt.executeQuery(sql);
- while (rs.next()) {
- Lot lot = new Lot();
- lot.lotId = rs.getInt("lot_id");
- lot.saleId = rs.getInt("sale_id");
- lot.currentBid = rs.getDouble("current_bid");
- lot.currency = rs.getString("currency");
- String closing = rs.getString("closing_time");
- lot.closingNotified = rs.getInt("closing_notified") != 0;
- if (closing != null) {
- lot.closingTime = LocalDateTime.parse(closing);
- }
- list.add(lot);
- }
- }
- return list;
- }
-
- /**
- * Retrieves all lots from the database.
- */
- synchronized List getAllLots() throws SQLException {
- List list = new ArrayList<>();
- String sql = "SELECT lot_id, sale_id, title, current_bid, currency FROM lots";
- try (Connection conn = DriverManager.getConnection(url); Statement stmt = conn.createStatement()) {
- ResultSet rs = stmt.executeQuery(sql);
- while (rs.next()) {
- Lot lot = new Lot();
- lot.lotId = rs.getInt("lot_id");
- lot.saleId = rs.getInt("sale_id");
- lot.title = rs.getString("title");
- lot.currentBid = rs.getDouble("current_bid");
- lot.currency = rs.getString("currency");
- list.add(lot);
- }
- }
- return list;
- }
-
- /**
- * Gets the total number of images in the database.
- */
- synchronized int getImageCount() throws SQLException {
- String sql = "SELECT COUNT(*) as count FROM images";
- try (Connection conn = DriverManager.getConnection(url); Statement stmt = conn.createStatement()) {
- ResultSet rs = stmt.executeQuery(sql);
- if (rs.next()) {
- return rs.getInt("count");
- }
- }
- return 0;
- }
-
- /**
- * Updates the current bid of a lot after a bid refresh.
- */
- synchronized void updateLotCurrentBid(Lot lot) throws SQLException {
- try (Connection conn = DriverManager.getConnection(url); PreparedStatement ps = conn.prepareStatement(
- "UPDATE lots SET current_bid = ? WHERE lot_id = ?")) {
- ps.setDouble(1, lot.currentBid);
- ps.setInt(2, lot.lotId);
- ps.executeUpdate();
- }
- }
-
- /**
- * Updates the closingNotified flag of a lot (set to 1 when we have
- * warned the user about its imminent closure).
- */
- synchronized void updateLotNotificationFlags(Lot lot) throws SQLException {
- try (Connection conn = DriverManager.getConnection(url); PreparedStatement ps = conn.prepareStatement(
- "UPDATE lots SET closing_notified = ? WHERE lot_id = ?")) {
- ps.setInt(1, lot.closingNotified ? 1 : 0);
- ps.setInt(2, lot.lotId);
- ps.executeUpdate();
- }
- }
- }
-
- /**
- * Service for sending notifications via desktop notifications and/or email.
- * Supports free notification methods:
- * 1. Desktop notifications (Windows/Linux/macOS system tray)
- * 2. Email via Gmail SMTP (free, requires app password)
- *
- * Configuration:
- * - For email: Set notificationEmail to your Gmail address
- * - Enable 2FA in Gmail and create an App Password
- * - Use format "smtp:username:appPassword:toEmail" for credentials
- * - Or use "desktop" for desktop-only notifications
- */
- static class NotificationService {
-
- private final boolean useDesktop;
- private final boolean useEmail;
- private final String smtpUsername;
- private final String smtpPassword;
- private final String toEmail;
-
- /**
- * Creates a notification service.
- *
- * @param config "desktop" for desktop only, or "smtp:username:password:toEmail" for email
- * @param unusedParam Kept for compatibility (can pass empty string)
- */
- NotificationService(String config, String unusedParam) {
-
- if ("desktop".equalsIgnoreCase(config)) {
- this.useDesktop = true;
- this.useEmail = false;
- this.smtpUsername = null;
- this.smtpPassword = null;
- this.toEmail = null;
- } else if (config.startsWith("smtp:")) {
- String[] parts = config.split(":", 4);
- if (parts.length != 4) {
- throw new IllegalArgumentException("Email config must be 'smtp:username:password:toEmail'");
- }
- this.useDesktop = true; // Always include desktop
- this.useEmail = true;
- this.smtpUsername = parts[1];
- this.smtpPassword = parts[2];
- this.toEmail = parts[3];
- } else {
- throw new IllegalArgumentException("Config must be 'desktop' or 'smtp:username:password:toEmail'");
- }
- }
-
- /**
- * Sends notification via configured channels.
- *
- * @param message The message body
- * @param title Message title
- * @param priority Priority level (0=normal, 1=high)
- */
- void sendNotification(String message, String title, int priority) {
- if (useDesktop) {
- sendDesktopNotification(title, message, priority);
- }
- if (useEmail) {
- sendEmailNotification(title, message, priority);
- }
- }
-
- /**
- * Sends a desktop notification using system tray.
- * Works on Windows, macOS, and Linux with desktop environments.
- */
- private void sendDesktopNotification(String title, String message, int priority) {
- try {
- if (java.awt.SystemTray.isSupported()) {
- java.awt.SystemTray tray = java.awt.SystemTray.getSystemTray();
- java.awt.Image image = java.awt.Toolkit.getDefaultToolkit()
- .createImage(new byte[0]); // Empty image
-
- java.awt.TrayIcon trayIcon = new java.awt.TrayIcon(image, "Troostwijk Scraper");
- trayIcon.setImageAutoSize(true);
-
- java.awt.TrayIcon.MessageType messageType = priority > 0
- ? java.awt.TrayIcon.MessageType.WARNING
- : java.awt.TrayIcon.MessageType.INFO;
-
- tray.add(trayIcon);
- trayIcon.displayMessage(title, message, messageType);
-
- // Remove icon after 2 seconds to avoid clutter
- Thread.sleep(2000);
- tray.remove(trayIcon);
-
- System.out.println("Desktop notification sent: " + title);
- } else {
- System.out.println("Desktop notifications not supported, logging: " + title + " - " + message);
- }
- } catch (Exception e) {
- System.err.println("Desktop notification failed: " + e.getMessage());
- }
- }
-
- /**
- * Sends email notification via Gmail SMTP (free).
- * Uses Gmail's SMTP server with app password authentication.
- */
- private void sendEmailNotification(String title, String message, int priority) {
- try {
- java.util.Properties props = new java.util.Properties();
- props.put("mail.smtp.auth", "true");
- props.put("mail.smtp.starttls.enable", "true");
- props.put("mail.smtp.host", "smtp.gmail.com");
- props.put("mail.smtp.port", "587");
- props.put("mail.smtp.ssl.trust", "smtp.gmail.com");
-
- javax.mail.Session session = javax.mail.Session.getInstance(props,
- new javax.mail.Authenticator() {
-
- protected javax.mail.PasswordAuthentication getPasswordAuthentication() {
- return new javax.mail.PasswordAuthentication(smtpUsername, smtpPassword);
- }
- });
-
- javax.mail.Message msg = new javax.mail.internet.MimeMessage(session);
- msg.setFrom(new javax.mail.internet.InternetAddress(smtpUsername));
- msg.setRecipients(javax.mail.Message.RecipientType.TO,
- javax.mail.internet.InternetAddress.parse(toEmail));
- msg.setSubject("[Troostwijk] " + title);
- msg.setText(message);
- msg.setSentDate(new java.util.Date());
-
- if (priority > 0) {
- msg.setHeader("X-Priority", "1");
- msg.setHeader("Importance", "High");
- }
-
- javax.mail.Transport.send(msg);
- System.out.println("Email notification sent: " + title);
-
- } catch (Exception e) {
- System.err.println("Email notification failed: " + e.getMessage());
- }
- }
- }
-
- /**
- * Service for performing object detection on images using OpenCV's DNN
- * module. The DNN module can load pre‑trained models from several
- * frameworks (Darknet, TensorFlow, ONNX, etc.)【784097309529506†L209-L233】. Here
- * we load a YOLO model (Darknet) by specifying the configuration and
- * weights files. For each image we run a forward pass and return a
- * list of detected class labels.
- *
- * If model files are not found, the service operates in disabled mode
- * and returns empty lists.
- */
- static class ObjectDetectionService {
-
- private final Net net;
- private final List classNames;
- private final boolean enabled;
-
- ObjectDetectionService(String cfgPath, String weightsPath, String classNamesPath) throws IOException {
- // Check if model files exist
- Path cfgFile = Paths.get(cfgPath);
- Path weightsFile = Paths.get(weightsPath);
- Path classNamesFile = Paths.get(classNamesPath);
-
- if (!Files.exists(cfgFile) || !Files.exists(weightsFile) || !Files.exists(classNamesFile)) {
- System.out.println("⚠️ Object detection disabled: YOLO model files not found");
- System.out.println(" Expected files:");
- System.out.println(" - " + cfgPath);
- System.out.println(" - " + weightsPath);
- System.out.println(" - " + classNamesPath);
- System.out.println(" Scraper will continue without image analysis.");
- this.enabled = false;
- this.net = null;
- this.classNames = new ArrayList<>();
- return;
- }
-
- try {
- // Load network
- this.net = Dnn.readNetFromDarknet(cfgPath, weightsPath);
- this.net.setPreferableBackend(DNN_BACKEND_OPENCV);
- this.net.setPreferableTarget(DNN_TARGET_CPU);
- // Load class names (one per line)
- this.classNames = Files.readAllLines(classNamesFile);
- this.enabled = true;
- System.out.println("✓ Object detection enabled with YOLO");
- } catch (Exception e) {
- System.err.println("⚠️ Object detection disabled: " + e.getMessage());
- throw new IOException("Failed to initialize object detection", e);
- }
- }
- /**
- * Detects objects in the given image file and returns a list of
- * human‑readable labels. Only detections above a confidence
- * threshold are returned. For brevity this method omits drawing
- * bounding boxes. See the OpenCV DNN documentation for details on
- * post‑processing【784097309529506†L324-L344】.
- *
- * @param imagePath absolute path to the image
- * @return list of detected class names (empty if detection disabled)
- */
- List detectObjects(String imagePath) {
- if (!enabled) {
- return new ArrayList<>();
- }
-
- List labels = new ArrayList<>();
- Mat image = Imgcodecs.imread(imagePath);
- if (image.empty()) return labels;
- // Create a 4D blob from the image
- Mat blob = Dnn.blobFromImage(image, 1.0 / 255.0, new Size(416, 416), new Scalar(0, 0, 0), true, false);
- net.setInput(blob);
- List outs = new ArrayList<>();
- List outNames = getOutputLayerNames(net);
- net.forward(outs, outNames);
- // Post‑process: for each detection compute score and choose class
- float confThreshold = 0.5f;
- for (Mat out : outs) {
- for (int i = 0; i < out.rows(); i++) {
- double[] data = out.get(i, 0);
- if (data == null) continue;
- // The first 5 numbers are bounding box, then class scores
- double[] scores = new double[classNames.size()];
- System.arraycopy(data, 5, scores, 0, scores.length);
- int classId = argMax(scores);
- double confidence = scores[classId];
- if (confidence > confThreshold) {
- String label = classNames.get(classId);
- if (!labels.contains(label)) {
- labels.add(label);
- }
- }
- }
- }
- return labels;
- }
- /**
- * Returns the indexes of the output layers in the network. YOLO
- * automatically discovers its output layers; other models may require
- * manually specifying them【784097309529506†L356-L365】.
- */
- private List getOutputLayerNames(Net net) {
- List names = new ArrayList<>();
- List outLayers = net.getUnconnectedOutLayers().toList();
- List layersNames = net.getLayerNames();
- for (Integer i : outLayers) {
- names.add(layersNames.get(i - 1));
- }
- return names;
- }
- /**
- * Returns the index of the maximum value in the array.
- */
- private int argMax(double[] array) {
- int best = 0;
- double max = array[0];
- for (int i = 1; i < array.length; i++) {
- if (array[i] > max) {
- max = array[i];
- best = i;
- }
- }
- return best;
- }
- }
}
\ No newline at end of file
diff --git a/src/test/java/com/auction/AuctionParsingTest.java b/src/test/java/com/auction/AuctionParsingTest.java
index 2464f38..370d06c 100644
--- a/src/test/java/com/auction/AuctionParsingTest.java
+++ b/src/test/java/com/auction/AuctionParsingTest.java
@@ -41,8 +41,8 @@ public class AuctionParsingTest {
System.out.println("\n=== Auction Parsing Test ===");
System.out.println("Found " + auctionLinks.size() + " auction links");
- List auctions = new ArrayList<>();
- int count = 0;
+ List auctions = new ArrayList<>();
+ int count = 0;
for (Element link : auctionLinks) {
String href = link.attr("href");
@@ -59,7 +59,7 @@ public class AuctionParsingTest {
int auctionId = Integer.parseInt(matcher.group(2));
// Extract auction info using IMPROVED text-based method
- TroostwijkScraper.AuctionInfo auction = extractAuctionInfoFromText(link, href, auctionId, "A" + typeNum);
+ AuctionInfo auction = extractAuctionInfoFromText(link, href, auctionId, "A" + typeNum);
auctions.add(auction);
// Print the first 10 auctions for verification
@@ -101,7 +101,7 @@ public class AuctionParsingTest {
assertTrue(auctions.size() > 0, "Should find at least one auction");
// Verify all auctions have basic info
- for (TroostwijkScraper.AuctionInfo auction : auctions) {
+ for (AuctionInfo auction : auctions) {
assertNotNull(auction.title, "Title should not be null for auction " + auction.auctionId);
assertTrue(auction.title.length() > 0, "Title should not be empty for auction " + auction.auctionId);
assertNotNull(auction.url, "URL should not be null for auction " + auction.auctionId);
@@ -119,8 +119,8 @@ public class AuctionParsingTest {
* Expected format: "[day] om [time] [lot_count] [title] [city], [CC]"
* Example: "woensdag om 18:00 1 Vrachtwagens voor bedrijfsvoertuigen Loßburg, DE"
*/
- private TroostwijkScraper.AuctionInfo extractAuctionInfoFromText(Element link, String href, int auctionId, String type) {
- TroostwijkScraper.AuctionInfo auction = new TroostwijkScraper.AuctionInfo();
+ private AuctionInfo extractAuctionInfoFromText(Element link, String href, int auctionId, String type) {
+ AuctionInfo auction = new AuctionInfo();
auction.auctionId = auctionId;
auction.type = type;
auction.url = "https://www.troostwijkauctions.com" + href;
diff --git a/src/test/java/com/auction/TroostwijkScraperTest.java b/src/test/java/com/auction/TroostwijkScraperTest.java
index 0b41454..1f4d226 100644
--- a/src/test/java/com/auction/TroostwijkScraperTest.java
+++ b/src/test/java/com/auction/TroostwijkScraperTest.java
@@ -68,71 +68,18 @@ public class TroostwijkScraperTest {
}
- @Test
- public void testFetchAndPersistAuctionData() throws SQLException {
- // First, discover auctions
- List auctions = scraper.discoverDutchAuctions();
- assertFalse(auctions.isEmpty(), "Need at least one auction to test");
-
- // Take the first auction and fetch its lots
- Integer firstSaleId = auctions.getFirst();
- System.out.println("Testing with sale ID: " + firstSaleId);
-
- scraper.fetchLotsForSale(firstSaleId);
-
- // Verify data was persisted to database
- List lotsInDb = scraper.db.getAllLots();
-
- assertNotNull(lotsInDb, "Lots list should not be null");
- assertFalse(lotsInDb.isEmpty(), "Should have persisted at least one lot");
-
- // Verify lot properties
- for (TroostwijkScraper.Lot lot : lotsInDb) {
- assertEquals(firstSaleId.intValue(), lot.saleId, "Lot should belong to the correct sale");
- assertTrue(lot.lotId > 0, "Lot ID should be positive");
- assertNotNull(lot.title, "Lot title should not be null");
- assertFalse(lot.title.isEmpty(), "Lot title should not be empty");
- assertNotNull(lot.url, "Lot URL should not be null");
- assertTrue(lot.url.startsWith("https://"), "Lot URL should be valid");
- assertTrue(lot.currentBid >= 0, "Current bid should be non-negative");
- }
-
- System.out.println("✓ Successfully persisted " + lotsInDb.size() + " lots to database");
- System.out.println("✓ All lot properties are valid");
- }
-
@Test
public void testDatabaseSchema() throws SQLException {
// Verify that the database schema was created correctly
- List lots = scraper.db.getAllLots();
+ List lots = scraper.db.getAllLots();
assertNotNull(lots, "Should be able to query lots table");
int imageCount = scraper.db.getImageCount();
assertTrue(imageCount >= 0, "Image count should be non-negative");
- List activeLots = scraper.db.getActiveLots();
+ List activeLots = scraper.db.getActiveLots();
assertNotNull(activeLots, "Should be able to query active lots");
System.out.println("✓ Database schema is valid and queryable");
}
-
- @Test
- public void testAuctionProperties() {
- List auctions = scraper.discoverDutchAuctions();
- assertFalse(auctions.isEmpty(), "Should find auctions");
-
- // Test that we can fetch data for multiple auctions
- int auctionsToTest = Math.min(2, auctions.size());
-
- for (int i = 0; i < auctionsToTest; i++) {
- Integer saleId = auctions.get(i);
- System.out.println("Testing auction " + (i + 1) + ": " + saleId);
-
- // This should not throw an exception
- assertDoesNotThrow(() -> scraper.fetchLotsForSale(saleId),
- "Should be able to fetch lots for sale " + saleId);
- }
-
- System.out.println("✓ Successfully tested " + auctionsToTest + " auctions");
- }
}
diff --git a/src/test/resources/test.md b/src/test/resources/test.md
index b0cee91..c6b0773 100644
--- a/src/test/resources/test.md
+++ b/src/test/resources/test.md
@@ -1,456 +1,61 @@
-## Woensdag 3 dec 25
-
-* [
-
- woensdag om 16:00
-
- 
-
- 
-
- 
-
- 
-
- 145
-
- Industrie & machines
-
- Meerdere locaties (45)
-
-
-
-
-
- ](/a/industrie-machines-A3-37358)
-* [
-
- woensdag om 16:00
-
- 
-
- 
-
- 
-
- 
-
- 38
-
- D | Raceautotransporters, kraan-polypengrepen en containers uit voorraadaanpassing
-
- Nieheim, DE
-
-
-
-
-
- ](/a/d-%7C-raceautotransporters-kraan-polypengrepen-en-containers-uit-voorraadaanpassing-A1-39772)
-* [
-
- woensdag om 16:00
-
- 
-
- 
-
- 
-
- 
-
- 61
-
- Voedselverwerkende apparatuur en verpakkingsmachines
-
- CHOMERAC, FR
-
-
-
-
-
- ](/a/voedselverwerkende-apparatuur-en-verpakkingsmachines-A1-39319)
-* [
-
- woensdag om 16:00
-
- 
-
- 
-
- 
-
- 
-
- 117
-
- Landbouw- & grondverzetmachines
-
- Meerdere locaties (49)
-
-
-
-
-
- ](/a/landbouw-grondverzetmachines-A3-37375)
-* [
-
- woensdag om 17:00
-
- 
-
- 
-
- 
-
- 
-
- 261
-
- Gereedschappen & uitrusting
-
- Meerdere locaties (36), BE
-
-
-
-
-
- ](/a/gereedschappen-uitrusting-A3-37367)
-* [
-
- woensdag om 18:00
-
- 
-
- 1
-
- Vrachtwagens voor bedrijfsvoertuigen
-
- Loßburg, DE
-
-
-
-
-
- ](/a/vrachtwagens-voor-bedrijfsvoertuigen-A7-39531)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 61
-
- Witgoed en accessoires
-
- Etten-Leur, NL
-
-
-
-
-
- ](/a/witgoed-en-accessoires-A1-27241)
-* [
-
- Opent 28 nov 17:00
-
- 
-
- 
-
- 
-
- 
-
- 54
-
- Collectie Rolex en Cartier horloges
-
- Dordrecht, NL
-
-
-
-
-
- ](/a/collectie-rolex-en-cartier-horloges-A1-39398)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 254
-
- SHOWROOMKEUKENS en INBOUWAPPARATUUR
-
- Tilburg, NL
-
-
-
-
-
- ](/a/showroomkeukens-en-inbouwapparatuur-A1-39480)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 499
-
- Machines, retourgoederen en restpartijen
-
- Harlingen, NL
-
-
-
-
-
- ](/a/machines-retourgoederen-en-restpartijen-A1-39642)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 120
-
- Partijen gereedschap, kantoorinventaris, detailhandelgoederen, decoratie en olijfbomen
-
- Meerdere locaties (3), NL
-
-
-
-
-
- ](/a/partijen-gereedschap-kantoorinventaris-detailhandelgoederen-decoratie-en-olijfbomen-A1-27016)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 16
-
- Faillissementsvoertuigen
-
- Meerdere locaties (3), NL
-
-
-
-
-
- ](/a/faillissementsvoertuigen-A1-38368)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 78
-
- Personenauto’s, oldtimers, campers en brommobielen
-
- Buitenpost, NL
-
-
-
-
-
- ](/a/personenauto%E2%80%99s-oldtimers-campers-en-brommobielen-A1-39508)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 391
-
- Bezorgveiling Faillissement Dvize B.V. – Hyundai Power Products gereedschappen
-
- Meerdere locaties (2)
-
-
-
-
-
- ](/a/bezorgveiling-faillissement-dvize-b-v-%E2%80%93-hyundai-power-products-gereedschappen-A1-39409)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 208
-
- Kunstplanten en bomen, composiet gevel- en vloerbekleding en akoestische materialen
-
- De Lier, NL
-
-
-
-
-
- ](/a/kunstplanten-en-bomen-composiet-gevel-en-vloerbekleding-en-akoestische-materialen-A1-28707)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 181
-
- Metaalbewerkingsmachines, gereedschap en voorraad in verband met bedrijfsverhuizing
-
- Cuijk, NL
-
-
-
-
-
- ](/a/metaalbewerkingsmachines-gereedschap-en-voorraad-in-verband-met-bedrijfsverhuizing-A1-39360)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 238
-
- Overstock en magazijnopruiming
-
- Heesch, NL
-
-
-
-
-
- ](/a/overstock-en-magazijnopruiming-A1-39538)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 47
-
- Verzamelveiling Scooters en Motoren
-
- Meerdere locaties (2), NL
-
-
-
-
-
- ](/a/verzamelveiling-scooters-en-motoren-A1-28428)
-* [
-
- woensdag om 19:00
-
- 
-
- 
-
- 
-
- 
-
- 338
-
- Auto's & transport
-
- Meerdere locaties (109)
-
-
-
-
-
- ](/a/auto%27s-transport-A3-37349)
-* [
-
- woensdag om 19:30
-
- 
-
- 
-
- 
-
- 
-
- 74
-
- Gouden juwelen en diamanten
-
- Meerdere locaties (28)
-
-
-
-
-
- ](/a/gouden-juwelen-en-diamanten-A1-29562)
\ No newline at end of file
+Configure your devices to use the Pi-hole as their DNS server │
+│ using: │
+│ │
+│ IPv4: 192.168.1.159 │
+│ IPv6: fdc5:59a6:9ac1:f11f:2c86:25d3:6282:37ef │
+│ If you have not done so already, the above IP should be set to │
+│ static. │
+│ View the web interface at http://pi.hole:80/admin or │
+│ http://192.168.1.159:80/admin │
+│ │
+│ Your Admin Webpage login password is gYj7Enh- │
+│ │
+│ │
+│ To allow your user to use all CLI functions without │
+│ authentication, │
+│ refer to https://docs.pi-hole.net/main/post-install/ │
+├─────────────────────────────────────────────────────────────
+
+
+127.0.0.1
+192.168.1.159
+::1
+fdc5:59a6:9ac1:f11f:2c86:25d3:6282:37ef
+fdc5:59a6:9ac1:f11f:bd8c:6e87:65f0:243c
+fe80::a05b:bbc6:d47f:3002%enp9s0
+2IXD-XJN9-C337-1K4Y-BBEO-HV1F-3BVI
+
+https://ollama.lan:9443/#!/wizard - heel-goed-wachtwoord
+
+[
+{
+"domain": "ollama.lan",
+"answer": "192.168.1.159",
+"enabled": true
+},
+{
+"domain": "hephaestus.lan",
+"answer": "192.168.1.159",
+"enabled": true
+},
+{
+"domain": "hermes.lan",
+"answer": "192.168.137.239",
+"enabled": true
+},
+{
+"domain": "atlas.lan",
+"answer": "192.168.1.100",
+"enabled": true
+},
+{
+"domain": "hub.lan",
+"answer": "192.168.1.1",
+"enabled": true
+},
+{
+"domain": "ha.lan",
+"answer": "192.168.1.193",
+"enabled": true
+}
+]
diff --git a/wiki/ARCHITECTURE-TROOSTWIJK-SCRAPER.md b/wiki/ARCHITECTURE-TROOSTWIJK-SCRAPER.md
new file mode 100644
index 0000000..98a9682
--- /dev/null
+++ b/wiki/ARCHITECTURE-TROOSTWIJK-SCRAPER.md
@@ -0,0 +1,326 @@
+# Troostwijk Scraper - Architecture & Data Flow
+
+## System Overview
+
+The scraper follows a **3-phase hierarchical crawling pattern** to extract auction and lot data from Troostwijk Auctions website.
+
+## Architecture Diagram
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ TROOSTWIJK SCRAPER │
+└─────────────────────────────────────────────────────────────────┘
+
+┌─────────────────────────────────────────────────────────────────┐
+│ PHASE 1: COLLECT AUCTION URLs │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ Listing Page │────────▶│ Extract /a/ │ │
+│ │ /auctions? │ │ auction URLs │ │
+│ │ page=1..N │ └──────────────┘ │
+│ └──────────────┘ │ │
+│ ▼ │
+│ [ List of Auction URLs ] │
+└─────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ PHASE 2: EXTRACT LOT URLs FROM AUCTIONS │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ Auction Page │────────▶│ Parse │ │
+│ │ /a/... │ │ __NEXT_DATA__│ │
+│ └──────────────┘ │ JSON │ │
+│ │ └──────────────┘ │
+│ │ │ │
+│ ▼ ▼ │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ Save Auction │ │ Extract /l/ │ │
+│ │ Metadata │ │ lot URLs │ │
+│ │ to DB │ └──────────────┘ │
+│ └──────────────┘ │ │
+│ ▼ │
+│ [ List of Lot URLs ] │
+└─────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ PHASE 3: SCRAPE LOT DETAILS │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ Lot Page │────────▶│ Parse │ │
+│ │ /l/... │ │ __NEXT_DATA__│ │
+│ └──────────────┘ │ JSON │ │
+│ └──────────────┘ │
+│ │ │
+│ ┌─────────────────────────┴─────────────────┐ │
+│ ▼ ▼ │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ Save Lot │ │ Save Images │ │
+│ │ Details │ │ URLs to DB │ │
+│ │ to DB │ └──────────────┘ │
+│ └──────────────┘ │ │
+│ ▼ │
+│ [Optional Download] │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+## Database Schema
+
+```sql
+┌──────────────────────────────────────────────────────────────────┐
+│ CACHE TABLE (HTML Storage with Compression) │
+├──────────────────────────────────────────────────────────────────┤
+│ cache │
+│ ├── url (TEXT, PRIMARY KEY) │
+│ ├── content (BLOB) -- Compressed HTML (zlib) │
+│ ├── timestamp (REAL) │
+│ ├── status_code (INTEGER) │
+│ └── compressed (INTEGER) -- 1=compressed, 0=plain │
+└──────────────────────────────────────────────────────────────────┘
+
+┌──────────────────────────────────────────────────────────────────┐
+│ AUCTIONS TABLE │
+├──────────────────────────────────────────────────────────────────┤
+│ auctions │
+│ ├── auction_id (TEXT, PRIMARY KEY) -- e.g. "A7-39813" │
+│ ├── url (TEXT, UNIQUE) │
+│ ├── title (TEXT) │
+│ ├── location (TEXT) -- e.g. "Cluj-Napoca, RO" │
+│ ├── lots_count (INTEGER) │
+│ ├── first_lot_closing_time (TEXT) │
+│ └── scraped_at (TEXT) │
+└──────────────────────────────────────────────────────────────────┘
+
+┌──────────────────────────────────────────────────────────────────┐
+│ LOTS TABLE │
+├──────────────────────────────────────────────────────────────────┤
+│ lots │
+│ ├── lot_id (TEXT, PRIMARY KEY) -- e.g. "A1-28505-5" │
+│ ├── auction_id (TEXT) -- FK to auctions │
+│ ├── url (TEXT, UNIQUE) │
+│ ├── title (TEXT) │
+│ ├── current_bid (TEXT) -- "€123.45" or "No bids" │
+│ ├── bid_count (INTEGER) │
+│ ├── closing_time (TEXT) │
+│ ├── viewing_time (TEXT) │
+│ ├── pickup_date (TEXT) │
+│ ├── location (TEXT) -- e.g. "Dongen, NL" │
+│ ├── description (TEXT) │
+│ ├── category (TEXT) │
+│ └── scraped_at (TEXT) │
+│ FOREIGN KEY (auction_id) → auctions(auction_id) │
+└──────────────────────────────────────────────────────────────────┘
+
+┌──────────────────────────────────────────────────────────────────┐
+│ IMAGES TABLE (Image URLs & Download Status) │
+├──────────────────────────────────────────────────────────────────┤
+│ images ◀── THIS TABLE HOLDS IMAGE LINKS│
+│ ├── id (INTEGER, PRIMARY KEY AUTOINCREMENT) │
+│ ├── lot_id (TEXT) -- FK to lots │
+│ ├── url (TEXT) -- Image URL │
+│ ├── local_path (TEXT) -- Path after download │
+│ └── downloaded (INTEGER) -- 0=pending, 1=downloaded │
+│ FOREIGN KEY (lot_id) → lots(lot_id) │
+└──────────────────────────────────────────────────────────────────┘
+```
+
+## Sequence Diagram
+
+```
+User Scraper Playwright Cache DB Data Tables
+ │ │ │ │ │
+ │ Run │ │ │ │
+ ├──────────────▶│ │ │ │
+ │ │ │ │ │
+ │ │ Phase 1: Listing Pages │ │
+ │ ├───────────────▶│ │ │
+ │ │ goto() │ │ │
+ │ │◀───────────────┤ │ │
+ │ │ HTML │ │ │
+ │ ├───────────────────────────────▶│ │
+ │ │ compress & cache │ │
+ │ │ │ │ │
+ │ │ Phase 2: Auction Pages │ │
+ │ ├───────────────▶│ │ │
+ │ │◀───────────────┤ │ │
+ │ │ HTML │ │ │
+ │ │ │ │ │
+ │ │ Parse __NEXT_DATA__ JSON │ │
+ │ │────────────────────────────────────────────────▶│
+ │ │ │ │ INSERT auctions
+ │ │ │ │ │
+ │ │ Phase 3: Lot Pages │ │
+ │ ├───────────────▶│ │ │
+ │ │◀───────────────┤ │ │
+ │ │ HTML │ │ │
+ │ │ │ │ │
+ │ │ Parse __NEXT_DATA__ JSON │ │
+ │ │────────────────────────────────────────────────▶│
+ │ │ │ │ INSERT lots │
+ │ │────────────────────────────────────────────────▶│
+ │ │ │ │ INSERT images│
+ │ │ │ │ │
+ │ │ Export to CSV/JSON │ │
+ │ │◀────────────────────────────────────────────────┤
+ │ │ Query all data │ │
+ │◀──────────────┤ │ │ │
+ │ Results │ │ │ │
+```
+
+## Data Flow Details
+
+### 1. **Page Retrieval & Caching**
+```
+Request URL
+ │
+ ├──▶ Check cache DB (with timestamp validation)
+ │ │
+ │ ├─[HIT]──▶ Decompress (if compressed=1)
+ │ │ └──▶ Return HTML
+ │ │
+ │ └─[MISS]─▶ Fetch via Playwright
+ │ │
+ │ ├──▶ Compress HTML (zlib level 9)
+ │ │ ~70-90% size reduction
+ │ │
+ │ └──▶ Store in cache DB (compressed=1)
+ │
+ └──▶ Return HTML for parsing
+```
+
+### 2. **JSON Parsing Strategy**
+```
+HTML Content
+ │
+ └──▶ Extract