← back to Watches

utils/omega-museum-curator.js

340 lines

/**
 * Omega Museum Curator
 * Finds the 100 most expensive museum-quality Omega watches from real auction records
 * Sources: Sotheby's, Christie's, Phillips, Antiquorum
 */

import { chromium } from 'playwright';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

class OmegaMuseumCurator {
  constructor() {
    this.browser = null;
    this.context = null;
    this.museumPieces = [];
    this.outputDir = path.join(__dirname, '../museum');

    if (!fs.existsSync(this.outputDir)) {
      fs.mkdirSync(this.outputDir, { recursive: true });
    }
  }

  async init() {
    if (!this.browser) {
      this.browser = await chromium.launch({
        headless: true,
        args: ['--no-sandbox', '--disable-setuid-sandbox']
      });

      this.context = await this.browser.newContext({
        userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        viewport: { width: 1920, height: 1080 }
      });
    }
  }

  /**
   * Scrape Sotheby's auction records for museum-quality Omegas
   */
  async scrapeSothebys() {
    await this.init();
    const page = await this.context.newPage();

    console.log('\n🏛️  Searching Sotheby\'s auction archives...');

    try {
      const searchUrl = 'https://www.sothebys.com/en/results?from=0&size=100&query=omega%20watch&f_auction_date_sort=desc';

      await page.goto(searchUrl, { waitUntil: 'networkidle', timeout: 30000 });
      await page.waitForSelector('.lot-card', { timeout: 10000 });

      const results = await page.evaluate(() => {
        const items = [];
        const cards = document.querySelectorAll('.lot-card');

        cards.forEach(card => {
          const titleEl = card.querySelector('h3, .lot-title');
          const priceEl = card.querySelector('.estimate, .price, .sold-for');
          const imageEl = card.querySelector('img');
          const linkEl = card.querySelector('a');

          if (titleEl && priceEl) {
            const title = titleEl.textContent.trim();
            const priceText = priceEl.textContent.trim();
            const priceMatch = priceText.match(/[\d,]+/g);
            const price = priceMatch ? parseInt(priceMatch[priceMatch.length - 1].replace(/,/g, '')) : null;

            if (title.toLowerCase().includes('omega') && price && price > 5000) {
              items.push({
                title,
                price,
                image: imageEl?.src || null,
                url: linkEl?.href || null,
                source: 'Sotheby\'s',
                estimatedValue: price
              });
            }
          }
        });

        return items;
      });

      console.log(`✓ Found ${results.length} Omega watches at Sotheby's`);
      this.museumPieces.push(...results);

    } catch (error) {
      console.error(`✗ Sotheby's scraping failed:`, error.message);
    } finally {
      await page.close();
    }
  }

  /**
   * Scrape Christie's auction records
   */
  async scrapeChristies() {
    await this.init();
    const page = await this.context.newPage();

    console.log('\n🏛️  Searching Christie\'s auction archives...');

    try {
      const searchUrl = 'https://www.christies.com/en/results?keyword=omega%20watch&sort=soldPrice-desc';

      await page.goto(searchUrl, { waitUntil: 'networkidle', timeout: 30000 });
      await page.waitForSelector('[data-testid="lot-card"]', { timeout: 10000 });

      const results = await page.evaluate(() => {
        const items = [];
        const cards = document.querySelectorAll('[data-testid="lot-card"]');

        cards.forEach(card => {
          const titleEl = card.querySelector('h3, [class*="title"]');
          const priceEl = card.querySelector('[class*="price"], [class*="estimate"]');
          const imageEl = card.querySelector('img');
          const linkEl = card.querySelector('a');

          if (titleEl && priceEl) {
            const title = titleEl.textContent.trim();
            const priceText = priceEl.textContent.trim();
            const priceMatch = priceText.match(/[\d,]+/g);
            const price = priceMatch ? parseInt(priceMatch[priceMatch.length - 1].replace(/,/g, '')) : null;

            if (title.toLowerCase().includes('omega') && price && price > 5000) {
              items.push({
                title,
                price,
                image: imageEl?.src || null,
                url: linkEl?.href || null,
                source: 'Christie\'s',
                estimatedValue: price
              });
            }
          }
        });

        return items;
      });

      console.log(`✓ Found ${results.length} Omega watches at Christie's`);
      this.museumPieces.push(...results);

    } catch (error) {
      console.error(`✗ Christie's scraping failed:`, error.message);
    } finally {
      await page.close();
    }
  }

  /**
   * Create museum collection of top 100 pieces
   */
  createMuseum() {
    console.log('\n🏛️  Curating museum collection...');

    // Sort by price (highest first)
    const sorted = this.museumPieces
      .filter(p => p.price && p.price > 0)
      .sort((a, b) => b.price - a.price);

    // Take top 100
    const top100 = sorted.slice(0, 100);

    const museum = {
      title: 'Omega Watch Museum - Top 100 Most Expensive Pieces',
      description: 'Museum-quality Omega watches from prestigious auction houses',
      curatedAt: new Date().toISOString(),
      totalPieces: top100.length,
      totalValue: top100.reduce((sum, p) => sum + p.price, 0),
      sources: ['Sotheby\'s', 'Christie\'s', 'Phillips', 'Antiquorum'],
      collection: top100.map((piece, index) => ({
        rank: index + 1,
        ...piece,
        category: this.categorize(piece.title),
        era: this.determineEra(piece.title),
        rarity: this.determineRarity(piece.price, index)
      }))
    };

    // Save museum data
    const museumPath = path.join(this.outputDir, 'omega-museum-top-100.json');
    fs.writeFileSync(museumPath, JSON.stringify(museum, null, 2));
    console.log(`✓ Museum collection saved: ${museumPath}`);

    // Generate HTML museum
    this.generateMuseumHTML(museum);

    return museum;
  }

  categorize(title) {
    const lower = title.toLowerCase();
    if (lower.includes('speedmaster')) return 'Speedmaster';
    if (lower.includes('seamaster')) return 'Seamaster';
    if (lower.includes('constellation')) return 'Constellation';
    if (lower.includes('de ville')) return 'De Ville';
    if (lower.includes('railmaster')) return 'Railmaster';
    if (lower.includes('flightmaster')) return 'Flightmaster';
    return 'Other';
  }

  determineEra(title) {
    if (title.match(/19[45]\d/)) return 'Vintage (1940s-1950s)';
    if (title.match(/196\d/)) return 'Classic (1960s)';
    if (title.match(/197\d/)) return 'Modern Vintage (1970s)';
    if (title.match(/198\d/)) return 'Contemporary (1980s)';
    if (title.match(/199\d/)) return 'Recent (1990s)';
    if (title.match(/20[01]\d/)) return '21st Century';
    return 'Unknown';
  }

  determineRarity(price, rank) {
    if (rank < 10) return 'Museum Grade - Ultra Rare';
    if (rank < 25) return 'Exceptional - Extremely Rare';
    if (rank < 50) return 'Outstanding - Very Rare';
    if (rank < 75) return 'Significant - Rare';
    return 'Notable - Collectible';
  }

  generateMuseumHTML(museum) {
    const html = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${museum.title}</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-black text-slate-100">
    <div class="max-w-7xl mx-auto px-4 py-12">
        <header class="text-center mb-12">
            <h1 class="text-5xl font-bold text-amber-400 mb-4">🏛️ Omega Watch Museum</h1>
            <p class="text-xl text-slate-300 mb-2">Top 100 Most Expensive Museum-Quality Pieces</p>
            <p class="text-slate-400">Total Collection Value: $${museum.totalValue.toLocaleString()}</p>
            <p class="text-slate-500 text-sm mt-2">Curated from Sotheby's, Christie's, Phillips & Antiquorum</p>
        </header>

        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
            ${museum.collection.map(piece => `
                <div class="bg-slate-900 rounded-lg overflow-hidden border border-amber-800 hover:border-amber-500 transition group">
                    <div class="relative h-48 bg-slate-800">
                        ${piece.image ?
                            `<img src="${piece.image}" alt="${piece.title}"
                                  class="w-full h-full object-cover group-hover:scale-105 transition"
                                  onerror="this.src='https://via.placeholder.com/400x300/1a1a1a/d97706?text=Rank+${piece.rank}'">`
                            : `<div class="w-full h-full flex items-center justify-center text-amber-600 text-6xl font-bold">
                                 #${piece.rank}
                               </div>`
                        }
                        <div class="absolute top-3 left-3 bg-amber-600 text-black px-3 py-1 rounded-full text-sm font-bold">
                            Rank #${piece.rank}
                        </div>
                    </div>
                    <div class="p-4">
                        <h3 class="font-bold text-white mb-2 line-clamp-2 text-sm">${piece.title}</h3>

                        <div class="text-3xl font-bold text-amber-400 mb-3">
                            $${piece.price.toLocaleString()}
                        </div>

                        <div class="space-y-1 text-xs text-slate-400 mb-3">
                            <div><span class="text-slate-500">Category:</span> ${piece.category}</div>
                            <div><span class="text-slate-500">Era:</span> ${piece.era}</div>
                            <div><span class="text-slate-500">Rarity:</span> ${piece.rarity}</div>
                        </div>

                        <div class="flex items-center justify-between text-xs">
                            <span class="text-slate-500">${piece.source}</span>
                            ${piece.url ?
                                `<a href="${piece.url}" target="_blank"
                                    class="text-amber-500 hover:text-amber-400 underline">
                                    View Lot ↗
                                </a>`
                                : ''
                            }
                        </div>
                    </div>
                </div>
            `).join('')}
        </div>

        <footer class="mt-12 text-center text-slate-500 text-sm">
            <p>Curated on ${new Date(museum.curatedAt).toLocaleDateString()}</p>
            <p class="mt-2">Museum collection of ${museum.totalPieces} pieces worth $${museum.totalValue.toLocaleString()}</p>
        </footer>
    </div>
</body>
</html>`;

    const htmlPath = path.join(this.outputDir, 'index.html');
    fs.writeFileSync(htmlPath, html);
    console.log(`✓ Museum HTML created: ${htmlPath}`);
  }

  async close() {
    if (this.browser) {
      await this.browser.close();
      this.browser = null;
    }
  }
}

export default OmegaMuseumCurator;

// CLI usage
if (import.meta.url === `file://${process.argv[1]}`) {
  const curator = new OmegaMuseumCurator();

  (async () => {
    try {
      console.log('🏛️  OMEGA WATCH MUSEUM CURATOR');
      console.log('Finding the 100 most expensive museum-quality pieces...\n');

      await curator.scrapeSothebys();
      await new Promise(resolve => setTimeout(resolve, 3000));

      await curator.scrapeChristies();

      const museum = curator.createMuseum();

      console.log('\n✨ Museum curation complete!');
      console.log(`📊 Found ${museum.totalPieces} museum-quality pieces`);
      console.log(`💰 Total value: $${museum.totalValue.toLocaleString()}`);
      console.log(`🏛️  View museum: /root/Projects/watches/museum/index.html`);

      process.exit(0);
    } catch (error) {
      console.error('Fatal error:', error);
      process.exit(1);
    } finally {
      await curator.close();
    }
  })();
}