← back to Whatsmystyle

scripts/clothing-apis/ebay.js

75 lines

/**
 * eBay Browse API adapter — secondhand / vintage.
 *
 * Endpoint: GET https://api.ebay.com/buy/browse/v1/item_summary/search
 * Key: Bearer <OAuth app token>. Free dev account at developer.ebay.com.
 *
 * Env:
 *   EBAY_OAUTH_TOKEN — required (production application token, refreshed via
 *                       client-credentials flow; out of scope for this file)
 *
 * One call yields up to 200 items per page. Free tier = 5000 calls/day, plenty.
 */
const fetch = global.fetch || require('node-fetch');

const TOKEN = process.env.EBAY_OAUTH_TOKEN;
const UA = 'WhatsMyStyleBot/0.1';
const PAGE_SIZE = 50;
const MAX_PAGES = 4;
const PAGE_DELAY_MS = 800;

async function importCategory(db, q) {
  if (!TOKEN) return { skipped: 'EBAY_OAUTH_TOKEN not set', source: `ebay:${q.id}` };

  const insert = db.prepare(`
    INSERT OR IGNORE INTO items
      (source, external_id, title, brand, category, price_cents, currency, image_url, product_url, tags, pro_grade)
    VALUES
      (@source, @external_id, @title, @brand, @category, @price_cents, @currency, @image_url, @product_url, @tags, @pro_grade)
  `);

  let imported = 0, fetched = 0;
  for (let page = 1; page <= MAX_PAGES; page++) {
    const offset = (page - 1) * PAGE_SIZE;
    const url = `https://api.ebay.com/buy/browse/v1/item_summary/search?category_ids=${q.category_id}&limit=${PAGE_SIZE}&offset=${offset}`;
    const r = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}`, 'User-Agent': UA, Accept: 'application/json' } });
    if (!r.ok) return { source: `ebay:${q.id}`, ok: false, status: r.status, imported };
    const j = await r.json();
    const items = j.itemSummaries || [];
    fetched += items.length;
    const tx = db.transaction(() => {
      for (const it of items) {
        const img = it.image?.imageUrl;
        const cents = it.price?.value ? Math.round(Number(it.price.value) * 100) : null;
        if (!img || !cents) continue;
        const category = /shoe|boot|sneaker/i.test(it.title) ? 'shoes'
                       : /dress|gown/i.test(it.title) ? 'dress'
                       : /coat|jacket|blazer/i.test(it.title) ? 'outerwear'
                       : /pant|trouser|jean|skirt/i.test(it.title) ? 'bottom'
                       : /bag|tote|purse/i.test(it.title) ? 'bag'
                       : 'top';
        const res = insert.run({
          source: `ebay:${q.id}`,
          external_id: `ebay:${it.itemId}`,
          title: (it.title || '').slice(0, 200),
          brand: it.brand || (it.seller?.username ? `eBay: ${it.seller.username}` : 'eBay seller'),
          category,
          price_cents: cents,
          currency: it.price?.currency || 'USD',
          image_url: img,
          product_url: it.itemWebUrl,
          tags: JSON.stringify(it.conditionId ? [`condition:${it.condition}`] : []),
          pro_grade: 0,        // one-off auction item — not restockable
        });
        if (res.changes > 0) imported++;
      }
    });
    tx();
    if (items.length < PAGE_SIZE) break;
    await new Promise(r => setTimeout(r, PAGE_DELAY_MS));
  }
  return { source: `ebay:${q.id}`, ok: true, fetched, imported };
}

module.exports = { importCategory };