import {IPage, ISearchItem, ISearchParams, ISearchProvider, Order, Sort, StationId} from "../type";
|
import {HTMLElement, parse} from "node-html-parser";
|
import {beginToPage, wrappedFetch} from "../lib";
|
|
|
/**
|
* 雅虎拍卖的搜索服务提供者。
|
*/
|
export class Provider4YahooAuction implements ISearchProvider {
|
|
readonly stationId = StationId.YahooAuction;
|
|
async search(params: ISearchParams): Promise<IPage> {
|
const warn: string[] = [];
|
const {keyword, begin = 1, maxNum = 50, category, minPrice, maxPrice, shipping, sort, order} = params;
|
const url = new URL("https://auctions.yahoo.co.jp/search/search");
|
url.searchParams.set("p", keyword);
|
url.searchParams.set("dest_pref_code", "13");
|
url.searchParams.set("b", String(begin));
|
url.searchParams.set("n", String(maxNum));
|
if (category) {
|
url.searchParams.set("auccat", category);
|
}
|
if (minPrice) {
|
url.searchParams.set("min", String(minPrice));
|
}
|
if (maxPrice) {
|
url.searchParams.set("max", String(maxPrice));
|
}
|
if (minPrice || maxPrice) {
|
url.searchParams.set("price_type", 'currentprice');
|
}
|
if (shipping === 'free') {
|
url.searchParams.set("shipping", '1');
|
}
|
if (sort) {
|
if (sort === Sort.Latest) {
|
url.searchParams.set("s1", 'new');
|
url.searchParams.set("o1", 'a');
|
} else if (sort === Sort.BidCount) {
|
url.searchParams.set("s1", 'bids');
|
url.searchParams.set("o1", 'a');
|
if (order === Order.Desc) {
|
warn.push(`sort=${sort} unsupported order=${order}`);
|
}
|
} else {
|
if (sort === Sort.Price) {
|
url.searchParams.set("s1", 'tbids');
|
} else if (sort === Sort.BuyItNow) {
|
url.searchParams.set("s1", 'tbidorbuy');
|
} else if (sort === Sort.RemainTime) {
|
url.searchParams.set("s1", 'end');
|
}
|
if (sort !== Sort.Default) {
|
if (order === Order.Desc) {
|
url.searchParams.set("o1", 'd');
|
} else {
|
url.searchParams.set("o1", 'a');
|
}
|
}
|
}
|
}
|
|
|
const resp = await wrappedFetch(url);
|
|
const pageNum = beginToPage(begin, maxNum);
|
|
// noinspection TypeScriptUnresolvedReference
|
const html = await resp.text();
|
|
if (!resp.ok) {
|
if (resp.status === 404 && resp.headers.get('content-type')?.includes('html')) {
|
const errorMessage = parseProductsError(html);
|
return {...params, dataList: [], error: errorMessage};
|
}
|
return {...params, dataList: [], error: `HTTP error! status: ${resp.status}`};
|
}
|
|
const {total, dataList} = parseProducts(html);
|
|
return {
|
warning: warn.join(', '),
|
srcUrl: url.toString(),
|
begin,
|
maxNum,
|
pageNum,
|
total,
|
...params,
|
dataList,
|
};
|
}
|
}
|
|
|
function parseProducts(docHtml: string): Omit<IPage, keyof ISearchParams> {
|
const map = new Map<string, ISearchItem>();
|
const doc = parse(docHtml);
|
doc.querySelectorAll('.Products__items .Product')
|
.map(parseItem)
|
.forEach(item => item.itemId && map.set(item.itemId, item));
|
|
const totalText = doc.querySelector('.Tab__item--current .Tab__subText')?.textContent;
|
const total = totalText ? parseInt(totalText.replace(/\D+/g, '')) : 0;
|
|
return {
|
total: Math.max(total || 0, map.size),
|
dataList: Array.from(map.values())
|
};
|
}
|
|
|
function parseItem(el: HTMLElement): ISearchItem {
|
const priceValueNodes = el.querySelectorAll('.Product__priceInfo .Product__priceValue');
|
const priceText = priceValueNodes[0]?.textContent?.replace(/\D+/g, '');
|
const buyItNowPriceText = priceValueNodes[1]?.textContent?.replace(/\D+/g, '');
|
const shippingFeeText = el.querySelector('.Product__priceInfo .Product__postage')?.textContent?.replace(/\D+/g, '');
|
|
return {
|
itemId: el.querySelector('a.Product__imageLink')?.getAttribute('data-auction-id'),
|
url: el.querySelector('a.Product__imageLink')?.getAttribute('href'),
|
image: {
|
imageUrl: el.querySelector('img.Product__imageData')?.getAttribute('src'),
|
alt: el.querySelector('img.Product__imageData')?.getAttribute('alt'),
|
},
|
name: el.querySelector('.Product__title')?.textContent?.trim(),
|
bid: el.querySelector('.Product__bid')?.textContent?.trim(),
|
time: el.querySelector('.Product__time')?.textContent?.trim(),
|
price: priceText ? parseInt(priceText) : undefined,
|
buyItNowPrice: buyItNowPriceText ? parseInt(buyItNowPriceText) : undefined,
|
shippingFee: shippingFeeText ? parseInt(shippingFeeText) : 0,
|
};
|
}
|
|
/**
|
* 从错误页面HTML中解析出错误信息。
|
* @param docHtml 错误页面的HTML文档字符串。
|
* @returns 返回错误信息字符串。
|
*/
|
function parseProductsError(docHtml: string): string {
|
const doc = parse(docHtml);
|
let msg = doc.querySelector('.SearchMode+div')?.text?.trim();
|
if (!msg) {
|
msg = doc.querySelector('.msg')?.text?.trim();
|
}
|
if (!msg) {
|
msg = doc.querySelector('#contents')?.text?.trim();
|
}
|
if (!msg) {
|
msg = doc.querySelector('body')?.text?.trim();
|
}
|
return msg || "Unknown error from Yahoo Auction";
|
}
|