From eb4a42025a72ae92ed91890c27737359ea37b0e8 Mon Sep 17 00:00:00 2001 From: Platane Date: Thu, 1 Aug 2019 12:36:59 +0200 Subject: [PATCH] feat: leboncoin.fr (#2749) * feat: add leboncoin.fr ad feed * docs: add leboncoin/ad/:query route --- docs/en/README.md | 12 ++++ lib/router.js | 3 + lib/routes/leboncoin/ad.js | 131 +++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 lib/routes/leboncoin/ad.js diff --git a/docs/en/README.md b/docs/en/README.md index dbb149186..7cac1d2bf 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -520,6 +520,18 @@ GitHub provides some official RSS feeds: +## E-commerce + +### leboncoin + +Transform any search into a feed. + + + +For instance, in https://www.leboncoin.fr/recherche/?**category=10&locations=Paris_75015**, the query is **category=10&locations=Paris_75015** + + + ## Uncategorized ### EZTV diff --git a/lib/router.js b/lib/router.js index a03a49e77..1a990b295 100644 --- a/lib/router.js +++ b/lib/router.js @@ -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')); diff --git a/lib/routes/leboncoin/ad.js b/lib/routes/leboncoin/ad.js new file mode 100644 index 000000000..8b6abfe46 --- /dev/null +++ b/lib/routes/leboncoin/ad.js @@ -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 && ``) || '', + '', + ...ad.body.split(/\n/), + '', + ((ad.images && ad.images.urls_thumb && ad.images.urls_thumb.map((url, i) => ``)) || []).join(''), + ].join('
'); + +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, + })), + }; +};