Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/routes/ebay/namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Namespace } from '@/types';

export const namespace: Namespace = {
name: 'eBay',
url: 'ebay.com',
categories: ['shopping'],
description: 'eBay search results and user listings.',
};
74 changes: 74 additions & 0 deletions lib/routes/ebay/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Route } from '@/types';
import { load } from 'cheerio';
import ofetch from '@/utils/ofetch';
import logger from '@/utils/logger';

export const route: Route = {
path: '/search/:keywords',
categories: ['shopping'],
example: '/ebay/search/sodimm+ddr4+16gb',
parameters: { keywords: 'Keywords for search' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['ebay.com/sch/i.html'],
target: (params, url) => {
const searchKeywords = new URL(url).searchParams.get('_nkw');
return `/search/${searchKeywords}`;
},
},
],
name: 'Search Results',
maintainers: ['phoeagon'],
handler: async (ctx) => {
const { keywords } = ctx.req.param();
const url = `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(keywords)}&_sop=10&_ipg=240`;

logger.info(`Fetching eBay search results: ${url}`);
const response = await ofetch(url);
logger.info(`eBay response status: ${response instanceof Response ? response.status : 'unknown'}`);
const $ = load(response);

const items = $('.s-item, .s-card, .s-item__wrapper.clearfix')
.toArray()
.map((item) => {
const $item = $(item);
const titleElement = $item.find('.s-item__title, .s-card__title, .s-item__title--has-tags');
const title = titleElement.text().replace(/^New Listing/i, '').trim();
const link = $item.find('.s-item__link, .s-card__link').attr('href');
const price = $item.find('.s-item__price, .s-card__price').text().trim();
const image =
$item.find('.s-item__image-img img, img.s-item__image-img').attr('src') ||
$item.find('.s-item__image-wrapper img').attr('src') ||
$item.find('.s-card__image-img img').attr('src') ||
$item.find('.s-item__image img').attr('src');

if (!title || !link || title.toLowerCase().includes('shop on ebay') || price === '') {
return null;
}

return {
title: `${title} - ${price}`,
link,
description: `<img src="${image}"><br>Price: ${price}`,
category: 'eBay Search',
};
})
.filter(Boolean);

logger.info(`Found ${items.length} items on eBay`);

return {
title: `eBay Search: ${keywords}`,
link: url,
item: items,
};
},
};
61 changes: 61 additions & 0 deletions lib/routes/ebay/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Route } from '@/types';
import { load } from 'cheerio';
import ofetch from '@/utils/ofetch';

export const route: Route = {
path: ['/usr/:username', '/user/:username'],
categories: ['shopping'],
example: '/ebay/usr/m.trotters',
parameters: { username: 'Username of the seller' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['ebay.com/usr/:username'],
target: '/user/:username',
},
],
name: 'User Listings',
maintainers: ['phoeagon'],
handler: async (ctx) => {
const { username } = ctx.req.param();
const url = `https://www.ebay.com/usr/${username}`;

const response = await ofetch(url);
const $ = load(response);

const items = $('article.str-item-card.StoreFrontItemCard')
.toArray()
.map((item) => {
const $item = $(item);
const title = $item.find('.str-card-title .str-text-span').first().text().trim();
const link = $item.find('.str-item-card__link').attr('href');
const price = $item.find('.str-item-card__property-displayPrice').text().trim();
const image = $item.find('.str-image img').attr('src');

if (!title || !link) {
return null;
}

return {
title: `${title} - ${price}`,
link,
description: `<img src="${image}"><br>Price: ${price}`,
author: username,
};
})
.filter(Boolean);

return {
title: `eBay User: ${username}`,
link: url,
item: items,
};
},
};
37 changes: 37 additions & 0 deletions lib/routes/ebay/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { CheerioAPI, Element } from 'cheerio';

/**
* Transforms an eBay image URL to prefer WebP format if it's a JPG/JPEG.
* @param url The original image URL.
* @returns The transformed URL.
*/
export const transformImage = (url?: string): string | undefined => {
if (!url) {
return undefined;
}
// eBay images often look like https://i.ebayimg.com/images/g/.../s-l500.jpg
// Replacing .jpg with .webp usually works if s-lXXX is used.
return url.replace(/\.jpe?g$/i, '.webp');
};

/**
* Common item structure for eBay routes.
*/
export interface eBayItem {
title: string;
link: string;
description: string;
category?: string;
author?: string;
}

/**
* Helper to extract common data from an eBay item element.
* Note: Since selectors vary, this might be less useful than specific logic in each route,
* but let's provide a way to standardize the output.
*/
export const createItem = (title: string, price: string, link: string, image?: string): eBayItem => ({
title: `${title} - ${price}`,
link,
description: `<img src="${transformImage(image)}"><br>Price: ${price}`,
});
Loading