feat(route/carousell): add carousell (#20677)
* feat(route/carousell): add carousell * feat(route/carousell): update search title format to include region
This commit is contained in:
parent
87bc71fd26
commit
dd7382a419
|
|
@ -0,0 +1,302 @@
|
|||
import type { Data, DataItem, Route } from '@/types';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
import type { ListingCard } from './types';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/:region/:keyword',
|
||||
categories: ['shopping'],
|
||||
example: '/carousell/sg/iphone',
|
||||
parameters: {
|
||||
region: {
|
||||
description: 'Region code',
|
||||
options: [
|
||||
{ value: 'au', label: 'Australia' },
|
||||
{ value: 'ca', label: 'Canada' },
|
||||
{ value: 'hk', label: 'Hong Kong' },
|
||||
{ value: 'id', label: 'Indonesia' },
|
||||
{ value: 'my', label: 'Malaysia' },
|
||||
{ value: 'nz', label: 'New Zealand' },
|
||||
{ value: 'ph', label: 'Philippines' },
|
||||
{ value: 'sg', label: 'Singapore' },
|
||||
{ value: 'tw', label: 'Taiwan' },
|
||||
],
|
||||
},
|
||||
keyword: {
|
||||
description: 'Search keyword',
|
||||
},
|
||||
},
|
||||
name: 'Keyword Search',
|
||||
maintainers: ['TonyRL'],
|
||||
handler,
|
||||
radar: [
|
||||
{ source: ['au.carousell.com/search/:keyword'], target: '/au/:keyword' },
|
||||
{ source: ['ca.carousell.com/search/:keyword'], target: '/ca/:keyword' },
|
||||
{ source: ['www.carousell.com.hk/search/:keyword'], target: '/hk/:keyword' },
|
||||
{ source: ['id.carousell.com/search/:keyword'], target: '/id/:keyword' },
|
||||
{ source: ['www.carousell.com.my/search/:keyword'], target: '/my/:keyword' },
|
||||
{ source: ['nz.carousell.com/search/:keyword'], target: '/nz/:keyword' },
|
||||
{ source: ['www.carousell.ph/search/:keyword'], target: '/ph/:keyword' },
|
||||
{ source: ['www.carousell.sg/search/:keyword'], target: '/sg/:keyword' },
|
||||
{ source: ['tw.carousell.com/search/:keyword'], target: '/tw/:keyword' },
|
||||
],
|
||||
};
|
||||
|
||||
const regionMap = {
|
||||
au: {
|
||||
api: 'https://au.carousell.com/ds/filter/cf/4.0/search/',
|
||||
baseUrl: 'https://au.carousell.com',
|
||||
payload: {
|
||||
bestMatchEnabled: false,
|
||||
canChangeKeyword: false,
|
||||
ccid: '1071',
|
||||
count: 48,
|
||||
countryCode: 'AU',
|
||||
countryId: '2077456',
|
||||
filters: [],
|
||||
includeBpEducationBanner: true,
|
||||
includeListingDescription: false,
|
||||
includePopularLocations: false,
|
||||
includeSuggestions: 'true',
|
||||
isCertifiedSpotlightEnabled: false,
|
||||
locale: 'en',
|
||||
prefill: { prefill_sort_by: '3' }, // most recent
|
||||
// query: '',
|
||||
sortParam: { fieldName: '3' },
|
||||
},
|
||||
|
||||
referer: (query) => `https://au.carousell.com/search/${query}?addRecent=true&canChangeKeyword=true&includeSuggestions=true&t-search_query_source=direct_search`,
|
||||
},
|
||||
ca: {
|
||||
api: 'https://ca.carousell.com/ds/filter/cf/4.0/search/',
|
||||
baseUrl: 'https://ca.carousell.com',
|
||||
payload: {
|
||||
bestMatchEnabled: false,
|
||||
canChangeKeyword: false,
|
||||
ccid: '976',
|
||||
count: 48,
|
||||
countryCode: 'CA',
|
||||
countryId: '6251999',
|
||||
filters: [],
|
||||
includeBpEducationBanner: true,
|
||||
includeListingDescription: false,
|
||||
includePopularLocations: false,
|
||||
includeSuggestions: 'true',
|
||||
isCertifiedSpotlightEnabled: false,
|
||||
locale: 'en',
|
||||
prefill: { prefill_sort_by: '3' },
|
||||
// query: '',
|
||||
sortParam: { fieldName: '3' },
|
||||
},
|
||||
referer: (query) => `https://ca.carousell.com/search/${query}?addRecent=true&canChangeKeyword=true&includeSuggestions=true&t-search_query_source=direct_search`,
|
||||
},
|
||||
hk: {
|
||||
api: 'https://www.carousell.com.hk/ds/filter/cf/4.0/search/',
|
||||
baseUrl: 'https://www.carousell.com.hk',
|
||||
payload: {
|
||||
bestMatchEnabled: true,
|
||||
canChangeKeyword: false,
|
||||
ccid: '5365',
|
||||
count: 48,
|
||||
countryCode: 'HK',
|
||||
countryId: '1819730',
|
||||
filters: [],
|
||||
includeBpEducationBanner: true,
|
||||
includeListingDescription: false,
|
||||
includePopularLocations: false,
|
||||
includeSuggestions: 'true',
|
||||
isCertifiedSpotlightEnabled: false,
|
||||
locale: 'zh-Hant-HK',
|
||||
prefill: { prefill_sort_by: '3' },
|
||||
// query: '',
|
||||
sortParam: { fieldName: '3' },
|
||||
},
|
||||
referer: (query) => `https://www.carousell.com.hk/search/${query}?addRecent=true&canChangeKeyword=true&includeSuggestions=true&t-search_query_source=direct_search`,
|
||||
},
|
||||
id: {
|
||||
api: 'https://id.carousell.com/ds/filter/cf/4.0/search/',
|
||||
baseUrl: 'https://id.carousell.com',
|
||||
payload: {
|
||||
bestMatchEnabled: true,
|
||||
canChangeKeyword: false,
|
||||
ccid: '726',
|
||||
count: 48,
|
||||
countryCode: 'ID',
|
||||
countryId: '1643084',
|
||||
filters: [],
|
||||
includeBpEducationBanner: true,
|
||||
includeListingDescription: false,
|
||||
includePopularLocations: false,
|
||||
includeSuggestions: 'true',
|
||||
isCertifiedSpotlightEnabled: false,
|
||||
locale: 'id',
|
||||
prefill: { prefill_sort_by: '3' },
|
||||
// query: '',
|
||||
sortParam: { fieldName: '3' },
|
||||
},
|
||||
referer: (query) => `https://id.carousell.com/search/${query}?addRecent=true&canChangeKeyword=true&includeSuggestions=true&t-search_query_source=direct_search`,
|
||||
},
|
||||
my: {
|
||||
api: 'https://www.carousell.com.my/ds/filter/cf/4.0/search/',
|
||||
baseUrl: 'https://www.carousell.com.my',
|
||||
payload: {
|
||||
bestMatchEnabled: true,
|
||||
canChangeKeyword: false,
|
||||
ccid: '6003',
|
||||
count: 48,
|
||||
countryCode: 'MY',
|
||||
countryId: '1733045',
|
||||
filters: [{ boolean: { value: false }, fieldName: '_delivery' }],
|
||||
includeBpEducationBanner: true,
|
||||
includeListingDescription: false,
|
||||
includePopularLocations: false,
|
||||
includeSuggestions: 'true',
|
||||
isCertifiedSpotlightEnabled: true,
|
||||
locale: 'en',
|
||||
prefill: { prefill_sort_by: '3' },
|
||||
// query: '',
|
||||
sortParam: { fieldName: '3' },
|
||||
},
|
||||
referer: (query) => `https://www.carousell.com.my/search/${query}?addRecent=true&canChangeKeyword=true&includeSuggestions=true&t-search_query_source=direct_search`,
|
||||
},
|
||||
nz: {
|
||||
api: 'https://nz.carousell.com/ds/filter/cf/4.0/search/',
|
||||
baseUrl: 'https://nz.carousell.com',
|
||||
payload: {
|
||||
bestMatchEnabled: false,
|
||||
canChangeKeyword: false,
|
||||
ccid: '1414',
|
||||
count: 48,
|
||||
countryCode: 'NZ',
|
||||
countryId: '2186224',
|
||||
filters: [],
|
||||
includeBpEducationBanner: true,
|
||||
includeListingDescription: false,
|
||||
includePopularLocations: false,
|
||||
includeSuggestions: 'true',
|
||||
isCertifiedSpotlightEnabled: false,
|
||||
locale: 'en',
|
||||
prefill: { prefill_sort_by: '3' },
|
||||
// query: '',
|
||||
sortParam: { fieldName: '3' },
|
||||
},
|
||||
referer: (query) => `https://nz.carousell.com/search/${query}?addRecent=true&canChangeKeyword=true&includeSuggestions=true&t-search_query_source=direct_search`,
|
||||
},
|
||||
ph: {
|
||||
api: 'https://www.carousell.ph/ds/filter/cf/4.0/search/',
|
||||
baseUrl: 'https://www.carousell.ph',
|
||||
payload: {
|
||||
bestMatchEnabled: true,
|
||||
canChangeKeyword: 'true',
|
||||
ccid: '5050',
|
||||
count: 48,
|
||||
countryCode: 'PH',
|
||||
countryId: '1694008',
|
||||
filters: [],
|
||||
includeBpEducationBanner: true,
|
||||
includeListingDescription: false,
|
||||
includePopularLocations: false,
|
||||
includeSuggestions: 'true',
|
||||
isCertifiedSpotlightEnabled: false,
|
||||
locale: 'en',
|
||||
prefill: { prefill_sort_by: '3' },
|
||||
// query: '',
|
||||
sortParam: { fieldName: '3' },
|
||||
},
|
||||
referer: (query) => `https://www.carousell.ph/search/${query}?addRecent=true&canChangeKeyword=true&includeSuggestions=true&t-search_query_source=direct_search`,
|
||||
},
|
||||
sg: {
|
||||
api: 'https://www.carousell.sg/ds/filter/cf/4.0/search/',
|
||||
baseUrl: 'https://www.carousell.sg',
|
||||
payload: {
|
||||
bestMatchEnabled: true,
|
||||
canChangeKeyword: 'true',
|
||||
ccid: '5727',
|
||||
count: 48,
|
||||
countryCode: 'SG',
|
||||
countryId: '1880251',
|
||||
filters: [{ boolean: { value: false }, fieldName: '_delivery' }],
|
||||
includeBpEducationBanner: true,
|
||||
includeListingDescription: false,
|
||||
includePopularLocations: false,
|
||||
includeSuggestions: 'true',
|
||||
isCertifiedSpotlightEnabled: false,
|
||||
locale: 'en',
|
||||
prefill: { prefill_sort_by: '3' },
|
||||
// query: '',
|
||||
sortParam: { fieldName: '3' },
|
||||
},
|
||||
referer: (query) => `https://www.carousell.sg/search/${query}?addRecent=true&canChangeKeyword=true&includeSuggestions=true&t-search_query_source=direct_search`,
|
||||
},
|
||||
tw: {
|
||||
api: 'https://tw.carousell.com/ds/filter/cf/4.0/search/',
|
||||
baseUrl: 'https://tw.carousell.com',
|
||||
payload: {
|
||||
bestMatchEnabled: true,
|
||||
canChangeKeyword: false,
|
||||
ccid: '6445',
|
||||
count: 48,
|
||||
countryCode: 'TW',
|
||||
countryId: '1668284',
|
||||
filters: [],
|
||||
includeBpEducationBanner: true,
|
||||
includeListingDescription: false,
|
||||
includePopularLocations: false,
|
||||
includeSuggestions: 'true',
|
||||
isCertifiedSpotlightEnabled: false,
|
||||
locale: 'zh-Hant-TW',
|
||||
prefill: { prefill_sort_by: '3' },
|
||||
// query: '',
|
||||
sortParam: { fieldName: '3' },
|
||||
},
|
||||
referer: (query) => `https://tw.carousell.com/search/${query}?addRecent=true&canChangeKeyword=true&includeSuggestions=true&t-search_query_source=direct_search`,
|
||||
},
|
||||
};
|
||||
|
||||
async function handler(ctx): Promise<Data> {
|
||||
const { region, keyword } = ctx.req.param();
|
||||
|
||||
if (!Object.keys(regionMap).includes(region)) {
|
||||
throw new Error(`Unsupported region code: ${region}`);
|
||||
}
|
||||
|
||||
const baseUrl = regionMap[region].baseUrl;
|
||||
const siteResponse = await ofetch.raw(baseUrl);
|
||||
const cookies = siteResponse.headers
|
||||
.getSetCookie()
|
||||
?.map((c) => c.split(';')[0])
|
||||
.join('; ');
|
||||
const csrfToken = siteResponse._data.match(/"csrfToken":"(.*?)","/)[1];
|
||||
|
||||
const response = await ofetch(regionMap[region].api, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
cookie: cookies,
|
||||
'csrf-token': csrfToken,
|
||||
referer: regionMap[region].referer(keyword),
|
||||
},
|
||||
body: {
|
||||
...regionMap[region].payload,
|
||||
query: keyword,
|
||||
},
|
||||
});
|
||||
|
||||
const items = response.data.results
|
||||
.filter((i) => i.listingCard)
|
||||
.map(({ listingCard: item }: { listingCard: ListingCard }) => ({
|
||||
title: item.title,
|
||||
description: `${item.photoUrls.map((url) => `<img src="${url.replace('_thumbnail.', '_progressive.')}">`).join('')}<br>${Object.values(item.belowFold)
|
||||
.map((v) => v.stringContent.replaceAll('\r', '<br>'))
|
||||
.join('<br>')}`,
|
||||
author: `${item.seller.firstName ?? ''} ${item.seller.lastName ?? ''} (@${item.seller.username})`.trim(),
|
||||
pubDate: parseDate(item.overlayContent.timestampContent.seconds.low, 'X'),
|
||||
guid: `${region}:${item.id}`,
|
||||
link: `${baseUrl}/p/${item.id}/`,
|
||||
})) satisfies DataItem[];
|
||||
|
||||
return {
|
||||
title: `Carousell ${region.toUpperCase()} Search - ${keyword}`,
|
||||
item: items,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: 'Carousell',
|
||||
url: 'carousell.com',
|
||||
lang: 'en',
|
||||
};
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
type TimestampSeconds = {
|
||||
low: number;
|
||||
high: number;
|
||||
unsigned: boolean;
|
||||
};
|
||||
type TimestampContent = {
|
||||
seconds: TimestampSeconds;
|
||||
};
|
||||
type Seller = {
|
||||
id: string;
|
||||
profilePicture: string;
|
||||
username: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
};
|
||||
type AboveFoldItem = {
|
||||
component: string;
|
||||
timestampContent: TimestampContent;
|
||||
};
|
||||
type BelowFoldItem = {
|
||||
component: string;
|
||||
stringContent: string;
|
||||
};
|
||||
type MarketPlaceId = {
|
||||
low: number;
|
||||
high: number;
|
||||
unsigned: boolean;
|
||||
};
|
||||
type CountryId = {
|
||||
low: number;
|
||||
high: number;
|
||||
unsigned: boolean;
|
||||
};
|
||||
type Country = {
|
||||
code: string;
|
||||
id: CountryId;
|
||||
name: string;
|
||||
};
|
||||
type Location = {
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
};
|
||||
type MarketPlace = {
|
||||
id: MarketPlaceId;
|
||||
name: string;
|
||||
country: Country;
|
||||
location: Location;
|
||||
};
|
||||
type Photo = {
|
||||
thumbnailUrl: string;
|
||||
thumbnailProgressiveUrl: string;
|
||||
thumbnailProgressiveLowRange: number;
|
||||
thumbnailProgressiveMediumRange: number;
|
||||
thumbnailHeight: number;
|
||||
thumbnailWidth: number;
|
||||
};
|
||||
type PhotoItem = {
|
||||
url: string;
|
||||
progressiveUrl: string;
|
||||
progressiveLowRange: number;
|
||||
progressiveMediumRange: number;
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
type VideoThumbnail = {
|
||||
url: string;
|
||||
progressiveUrl: string;
|
||||
progressiveLowRange: number;
|
||||
progressiveMediumRange: number;
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
type SupportedFormat = {
|
||||
dash: string;
|
||||
hls: string;
|
||||
};
|
||||
type PlayConfig = {
|
||||
isLoop: boolean;
|
||||
onlyWifi: boolean;
|
||||
isAutoPlay: boolean;
|
||||
isMuted: boolean;
|
||||
};
|
||||
type VideoItem = {
|
||||
thumbnail: VideoThumbnail;
|
||||
supportedFormat: SupportedFormat;
|
||||
playConfig: PlayConfig;
|
||||
};
|
||||
type MediaItem = {
|
||||
photoItem?: PhotoItem;
|
||||
videoItem?: VideoItem;
|
||||
};
|
||||
type OverlayContent = {
|
||||
timestampContent: TimestampContent;
|
||||
iconUrl?: {
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
type OriginalPrice = {
|
||||
value: string;
|
||||
};
|
||||
type Tag = {
|
||||
content: string;
|
||||
backgroundColor: string;
|
||||
fontColor: string;
|
||||
};
|
||||
export type ListingCard = {
|
||||
id: string;
|
||||
seller: Seller;
|
||||
photoUrls: string[];
|
||||
aboveFold: AboveFoldItem[];
|
||||
belowFold: BelowFoldItem[];
|
||||
status: string;
|
||||
marketPlace: MarketPlace;
|
||||
photos: Photo[];
|
||||
media: MediaItem[];
|
||||
price: string;
|
||||
title: string;
|
||||
overlayContent: OverlayContent;
|
||||
isSellerVisible: boolean;
|
||||
countryCollectionId: string;
|
||||
ctaButtons: string[];
|
||||
likesCount?: number;
|
||||
originalPrice?: OriginalPrice;
|
||||
cardType?: number;
|
||||
tags?: Tag[];
|
||||
};
|
||||
Loading…
Reference in New Issue