← back to Whatsmystyle

scripts/clothing-apis/etsy.js

74 lines

/**
 * Etsy Open API v3 adapter — vintage / handmade.
 *
 * Endpoint: GET https://openapi.etsy.com/v3/application/listings/active
 * Key:      x-api-key header. Free key — apply at https://www.etsy.com/developers/register
 *
 * Env:
 *   ETSY_API_KEY — required; without it this adapter returns { skipped: 'no key' }
 *
 * One-call yield is ~25 listings; we paginate up to MAX_PAGES.
 */
const fetch = global.fetch || require('node-fetch');

const KEY = process.env.ETSY_API_KEY;
const UA = 'WhatsMyStyleBot/0.1';
const PAGE_SIZE = 25;
const MAX_PAGES = 4;
const PAGE_DELAY_MS = 1000;

async function importQuery(db, q) {
  if (!KEY) return { skipped: 'ETSY_API_KEY not set', source: `etsy:${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://openapi.etsy.com/v3/application/listings/active?keywords=${encodeURIComponent(q.query)}&limit=${PAGE_SIZE}&offset=${offset}&includes=Images`;
    const r = await fetch(url, { headers: { 'x-api-key': KEY, 'User-Agent': UA } });
    if (!r.ok) return { source: `etsy:${q.id}`, ok: false, status: r.status, imported };
    const j = await r.json();
    const items = j.results || [];
    fetched += items.length;
    const tx = db.transaction(() => {
      for (const it of items) {
        const img = it.images?.[0]?.url_fullxfull || it.images?.[0]?.url_570xN;
        const cents = Math.round(Number(it.price?.amount || 0) / Number(it.price?.divisor || 1) * 100);
        if (!img || !cents) continue;
        // Etsy doesn't ship a tight category enum; bucket by query intent.
        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'
                       : 'top';
        const res = insert.run({
          source: `etsy:${q.id}`,
          external_id: `etsy:${it.listing_id}`,
          title: (it.title || '').slice(0, 200),
          brand: it.shop_id ? `Etsy shop #${it.shop_id}` : 'Etsy seller',
          category,
          price_cents: cents,
          currency: it.price?.currency_code || 'USD',
          image_url: img,
          product_url: `https://www.etsy.com/listing/${it.listing_id}`,
          tags: JSON.stringify(it.tags || []),
          pro_grade: 0,        // vintage / one-off — set decorators won't restock
        });
        if (res.changes > 0) imported++;
      }
    });
    tx();
    if (items.length < PAGE_SIZE) break;
    await new Promise(r => setTimeout(r, PAGE_DELAY_MS));
  }
  return { source: `etsy:${q.id}`, ok: true, fetched, imported };
}

module.exports = { importQuery };