feat: leboncoin.fr (#2749)

* feat: add leboncoin.fr ad feed

* docs: add leboncoin/ad/:query route
This commit is contained in:
Platane
2019-08-01 12:36:59 +02:00
committed by DIYgod
parent ae3e6777b4
commit eb4a42025a
3 changed files with 146 additions and 0 deletions

View File

@@ -520,6 +520,18 @@ GitHub provides some official RSS feeds:
<RouteEn author="HenryQW" example="/parcel/hermesuk/[tracking number]" path="/parcel/hermesuk/:tracking" :paramsDesc="['Tracking number']"/>
## E-commerce
### leboncoin
Transform any search into a feed.
<RouteEn author="Platane" example="/leboncoin/ad/category=10&locations=Paris_75015" path="/leboncoin/ad/:query" :paramsDesc="['search page querystring']">
For instance, in https://www.leboncoin.fr/recherche/?**category=10&locations=Paris_75015**, the query is **category=10&locations=Paris_75015**
</RouteEn>
## Uncategorized
### EZTV

View File

@@ -1597,6 +1597,9 @@ router.get('/aliyun/database_month', require('./routes/aliyun/database_month'));
// 礼物说
router.get('/liwushuo/index', require('./routes/liwushuo/index.js'));
// leboncoin
router.get('/leboncoin/ad/:query', require('./routes/leboncoin/ad.js'));
// DHL
router.get('/dhl/:id', require('./routes/dhl/shipment-tracking'));

131
lib/routes/leboncoin/ad.js Normal file
View File

@@ -0,0 +1,131 @@
const querystring = require('querystring');
const got = require('@/utils/got');
const API_KEY = 'ba0c2dad52b3ec';
const SEARCH_ENDPOINT = 'https://api.leboncoin.fr/finder/search';
// convert the querystring read from the url to the object expected by the rest API
const convertQueryToFilters = (query) => {
const queryObject = querystring.parse(query);
const filters = { keywords: {}, location: {}, ranges: {}, enums: {} };
queryObject.ad_type = queryObject.ad_type || 'offer';
for (const [key, value] of Object.entries(queryObject)) {
// category
if (key === 'category') {
filters.category = { id: queryObject.category };
continue;
}
// location.locations
if (key === 'locations') {
filters.location.locations = value
.split(',')
.map((l) => {
if (l.startsWith('r_')) {
return { locationType: 'region', region_id: l.slice(2) };
}
if (l.startsWith('dn_')) {
return { locationType: 'department_near', department_id: l.slice(3) };
}
{
if (l.match(/.+_.+/)) {
const [city, zipcode] = l.split('_');
return { locationType: 'city', city, zipcode };
}
}
return undefined;
})
.filter(Boolean);
continue;
}
// keywords.text
if (key === 'text') {
filters.keywords.text = value;
continue;
}
// location.area
if (key === 'lat' || key === 'lng' || key === 'radius') {
filters.location.area = {
lat: parseFloat(queryObject.lat),
lng: parseFloat(queryObject.lng),
radius: parseFloat(queryObject.radius),
};
continue;
}
// range type
{
const m = value.match(/^(\d+|min)-(\d+|max)$/);
if (m) {
const [, min, max] = m;
filters.ranges = filters.ranges || {};
filters.ranges[key] = {};
if (min !== 'min') {
filters.ranges[key].min = parseInt(min);
}
if (max !== 'max') {
filters.ranges[key].max = parseInt(max);
}
continue;
}
}
// enum type
filters.enums[key] = value.split(',');
}
return filters;
};
const buildDescription = (ad) =>
// the description is made of
// - first the thumbnail
// - the ad description
// - all the images in thumbnail version, and link to large version
[
(ad.images && ad.images.thumb_url && `<img referrerpolicy="no-referrer" src="${ad.images.thumb_url}">`) || '',
'',
...ad.body.split(/\n/),
'',
((ad.images && ad.images.urls_thumb && ad.images.urls_thumb.map((url, i) => `<a href=${ad.images.urls_large[i]}><img referrerpolicy="no-referrer" src="${url}"></a>`)) || []).join(''),
].join('<br>');
module.exports = async (ctx) => {
const query = ctx.params.query;
const response = await got({
headers: {
api_key: API_KEY,
origin: 'https://www.leboncoin.fr',
},
method: 'post',
url: SEARCH_ENDPOINT,
body: JSON.stringify({ filters: convertQueryToFilters(query), limit: 50, limit_alu: 3, sort_by: 'time', sort_order: 'desc' }),
});
ctx.state.data = {
title: `ads for ${query.replace(/&/g, ' ')}`,
description: `ads for ${query.replace(/&/g, ' ')}`,
link: `https://www.leboncoin.fr/recherche/?${query}`,
language: 'fr',
item: (response.data.ads || [])
.filter((ad) => ad.status === 'active')
.map((ad) => ({
title: (ad.price && ad.price[0] ? `${ad.price[0]}€ - ` : '') + ad.subject,
description: buildDescription(ad),
pubDate: new Date(ad.first_publication_date).toUTCString(),
link: ad.url,
})),
};
};