← back to Handbag Authentication
api/ebay-sold-data.js
231 lines
/**
* eBay Sold Data Fetcher
* Fetches actual sold/completed listings from eBay USA and eBay Japan
*/
const axios = require('axios');
const cheerio = require('cheerio');
class EbaySoldDataFetcher {
constructor() {
// eBay Finding API credentials (if available)
this.ebayAppId = process.env.EBAY_APP_ID || null;
}
/**
* Fetch sold listings from eBay USA
*/
async fetchEbayUSASold(brand, model, limit = 10) {
try {
// Method 1: Use eBay Finding API if credentials available
if (this.ebayAppId) {
return await this.fetchViaAPI(brand, model, 'EBAY-US', limit);
}
// Method 2: Scrape sold listings page
const searchQuery = `${brand} ${model} bag`;
const url = `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(searchQuery)}&LH_Sold=1&LH_Complete=1&_sop=13`; // Sort by recently sold
console.log(`Fetching eBay USA sold data: ${url}`);
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
timeout: 15000
});
const $ = cheerio.load(response.data);
const soldItems = [];
$('.s-item').each((i, elem) => {
if (i >= limit) return false;
const title = $(elem).find('.s-item__title').text().trim();
const priceText = $(elem).find('.s-item__price').text().trim();
const link = $(elem).find('.s-item__link').attr('href');
const soldDate = $(elem).find('.s-item__title--tag').text().trim();
const image = $(elem).find('.s-item__image-img').attr('src');
// Parse price
const priceMatch = priceText.match(/\$([0-9,]+\.?\d*)/);
const price = priceMatch ? parseFloat(priceMatch[1].replace(/,/g, '')) : null;
if (title && price && title.toLowerCase() !== 'shop on ebay') {
soldItems.push({
platform: 'eBay USA',
title,
price,
currency: 'USD',
soldDate,
url: link,
image,
source: 'ebay_usa'
});
}
});
console.log(`✓ Found ${soldItems.length} eBay USA sold items`);
return soldItems;
} catch (error) {
console.error('Error fetching eBay USA sold data:', error.message);
return [];
}
}
/**
* Fetch sold listings from eBay Japan
*/
async fetchEbayJapanSold(brand, model, limit = 10) {
try {
const searchQuery = `${brand} ${model} バッグ`;
const url = `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(searchQuery)}&LH_Sold=1&LH_Complete=1&LH_PrefLoc=2&_sop=13`; // LH_PrefLoc=2 = Japan
console.log(`Fetching eBay Japan sold data: ${url}`);
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
timeout: 15000
});
const $ = cheerio.load(response.data);
const soldItems = [];
$('.s-item').each((i, elem) => {
if (i >= limit) return false;
const title = $(elem).find('.s-item__title').text().trim();
const priceText = $(elem).find('.s-item__price').text().trim();
const link = $(elem).find('.s-item__link').attr('href');
const soldDate = $(elem).find('.s-item__title--tag').text().trim();
const image = $(elem).find('.s-item__image-img').attr('src');
// Parse price (could be USD or JPY)
let price = null;
let currency = 'USD';
const usdMatch = priceText.match(/\$([0-9,]+\.?\d*)/);
const jpyMatch = priceText.match(/¥([0-9,]+)/);
if (usdMatch) {
price = parseFloat(usdMatch[1].replace(/,/g, ''));
currency = 'USD';
} else if (jpyMatch) {
price = parseFloat(jpyMatch[1].replace(/,/g, ''));
currency = 'JPY';
}
if (title && price && title.toLowerCase() !== 'shop on ebay') {
soldItems.push({
platform: 'eBay Japan',
title,
price,
currency,
soldDate,
url: link,
image,
source: 'ebay_japan'
});
}
});
console.log(`✓ Found ${soldItems.length} eBay Japan sold items`);
return soldItems;
} catch (error) {
console.error('Error fetching eBay Japan sold data:', error.message);
return [];
}
}
/**
* Fetch via eBay Finding API (if credentials available)
*/
async fetchViaAPI(brand, model, site = 'EBAY-US', limit = 10) {
try {
const keywords = `${brand} ${model} bag`;
const url = `https://svcs.ebay.com/services/search/FindingService/v1?OPERATION-NAME=findCompletedItems&SERVICE-VERSION=1.0.0&SECURITY-APPNAME=${this.ebayAppId}&RESPONSE-DATA-FORMAT=JSON&keywords=${encodeURIComponent(keywords)}&GLOBAL-ID=${site}&paginationInput.entriesPerPage=${limit}&sortOrder=EndTimeSoonest&itemFilter(0).name=SoldItemsOnly&itemFilter(0).value=true`;
const response = await axios.get(url, { timeout: 15000 });
const items = response.data.findCompletedItemsResponse?.[0]?.searchResult?.[0]?.item || [];
return items.map(item => ({
platform: site === 'EBAY-US' ? 'eBay USA' : 'eBay Japan',
title: item.title?.[0] || '',
price: parseFloat(item.sellingStatus?.[0]?.currentPrice?.[0]?.__value__ || 0),
currency: item.sellingStatus?.[0]?.currentPrice?.[0]?.['@currencyId'] || 'USD',
soldDate: item.listingInfo?.[0]?.endTime?.[0] || '',
url: item.viewItemURL?.[0] || '',
image: item.galleryURL?.[0] || '',
source: site === 'EBAY-US' ? 'ebay_usa' : 'ebay_japan'
}));
} catch (error) {
console.error('Error fetching via eBay API:', error.message);
return [];
}
}
/**
* Fetch from all sources and combine
*/
async fetchAllSoldData(brand, model) {
console.log(`\n🔍 Fetching sold data for: ${brand} ${model}`);
const [ebayUSA, ebayJapan] = await Promise.all([
this.fetchEbayUSASold(brand, model, 10),
this.fetchEbayJapanSold(brand, model, 10)
]);
const allSold = [...ebayUSA, ...ebayJapan];
// Sort by price (high to low)
allSold.sort((a, b) => {
const priceA = a.currency === 'JPY' ? a.price / 150 : a.price;
const priceB = b.currency === 'JPY' ? b.price / 150 : b.price;
return priceB - priceA;
});
console.log(`✅ Total sold items found: ${allSold.length}`);
return allSold;
}
/**
* Calculate statistics from sold data
*/
calculateStats(soldItems) {
if (soldItems.length === 0) {
return {
count: 0,
avgPrice: 0,
highPrice: 0,
lowPrice: 0,
medianPrice: 0
};
}
// Convert all to USD
const pricesUSD = soldItems.map(item =>
item.currency === 'JPY' ? item.price / 150 : item.price
);
pricesUSD.sort((a, b) => a - b);
const sum = pricesUSD.reduce((a, b) => a + b, 0);
const median = pricesUSD[Math.floor(pricesUSD.length / 2)];
return {
count: soldItems.length,
avgPrice: Math.round(sum / pricesUSD.length),
highPrice: Math.round(Math.max(...pricesUSD)),
lowPrice: Math.round(Math.min(...pricesUSD)),
medianPrice: Math.round(median)
};
}
}
module.exports = EbaySoldDataFetcher;