← back to Handbag Authentication

api/yahoo-data-extractor.js

167 lines

const puppeteer = require('puppeteer');

class YahooDataExtractor {
  constructor() {
    this.browser = null;
  }

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

  async extractFullDetails(yahooUrl) {
    await this.init();
    const page = await this.browser.newPage();

    try {
      await page.goto(yahooUrl, { waitUntil: 'networkidle2', timeout: 30000 });

      const details = await page.evaluate(() => {
        const getText = (selector) => {
          const el = document.querySelector(selector);
          return el ? el.textContent.trim() : null;
        };

        const getAllText = (selector) => {
          const els = document.querySelectorAll(selector);
          return Array.from(els).map(el => el.textContent.trim());
        };

        // Extract title
        const title = getText('h1.ProductTitle__text') ||
                     getText('.ProductTitle') ||
                     getText('h1');

        // Extract description
        const description = getText('.ProductExplanation__body') ||
                          getText('.ProductExplanation') ||
                          getText('[class*="explanation"]') ||
                          getText('[class*="description"]');

        // Extract product details table
        const detailRows = {};
        document.querySelectorAll('.ProductDetail__item, .Product__detail tr').forEach(row => {
          const label = row.querySelector('dt, th, .label');
          const value = row.querySelector('dd, td:last-child, .value');
          if (label && value) {
            detailRows[label.textContent.trim()] = value.textContent.trim();
          }
        });

        // Extract images
        const images = [];
        document.querySelectorAll('.ProductImage__image img, .Product__image img, [class*="ProductImage"] img').forEach(img => {
          if (img.src && !img.src.includes('noimage')) {
            images.push(img.src.replace(/\.(jpg|jpeg|png).*$/, '.$1')); // Clean URL params
          }
        });

        // Extract condition
        const condition = getText('.Product__condition') ||
                         getText('[class*="condition"]') ||
                         detailRows['状態'] ||
                         detailRows['商品の状態'] ||
                         '';

        // Extract brand from various places
        const brand = detailRows['ブランド'] ||
                     detailRows['メーカー'] ||
                     getText('.Product__brand') ||
                     '';

        // Extract model
        const model = detailRows['モデル'] ||
                     detailRows['型番'] ||
                     detailRows['品番'] ||
                     '';

        // Extract size
        const size = detailRows['サイズ'] ||
                    detailRows['Size'] ||
                    '';

        // Extract color
        const color = detailRows['カラー'] ||
                     detailRows['色'] ||
                     detailRows['Color'] ||
                     '';

        // Extract material
        const material = detailRows['素材'] ||
                        detailRows['Material'] ||
                        '';

        // Extract serial number
        const serial = detailRows['シリアルナンバー'] ||
                      detailRows['製造番号'] ||
                      detailRows['Serial'] ||
                      '';

        // Extract all condition notes
        const conditionNotes = getAllText('.Product__condition p, [class*="condition"] p, [class*="note"]');

        return {
          title,
          description,
          brand,
          model,
          size,
          color,
          material,
          serial,
          condition,
          conditionNotes: conditionNotes.join(' '),
          images,
          detailTable: detailRows,
          extractedAt: new Date().toISOString()
        };
      });

      await page.close();
      return details;

    } catch (error) {
      console.error('Yahoo extraction error:', error);
      await page.close();
      return null;
    }
  }

  async translateCondition(japaneseCondition) {
    const conditionMap = {
      '未使用': 'New with tags',
      '新品': 'New',
      '新品同様': 'Like new',
      '極美品': 'Excellent',
      '美品': 'Very good',
      'AB': 'Good',
      'BC': 'Fair',
      'CD': 'Poor',
      '中古': 'Used',
      'ジャンク': 'For parts'
    };

    for (const [jp, en] of Object.entries(conditionMap)) {
      if (japaneseCondition.includes(jp)) {
        return en;
      }
    }

    return 'Used';
  }

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

module.exports = YahooDataExtractor;