|
|
const axios = require('axios'); |
|
|
const FormData = require('form-data'); |
|
|
const crypto = require('crypto'); |
|
|
|
|
|
class PinterestLensScraper { |
|
|
constructor(authToken) { |
|
|
this.authToken = authToken; |
|
|
} |
|
|
|
|
|
getRandomHeaders() { |
|
|
const devices = [ |
|
|
{ model: 'SM-G991B', manufacturer: 'samsung', name: 'Samsung Galaxy S21' }, |
|
|
{ model: 'SM-A525F', manufacturer: 'samsung', name: 'Samsung Galaxy A52' }, |
|
|
{ model: 'Pixel 6', manufacturer: 'Google', name: 'Google Pixel 6' }, |
|
|
{ model: 'Pixel 7 Pro', manufacturer: 'Google', name: 'Google Pixel 7 Pro' }, |
|
|
{ model: 'M2101K6G', manufacturer: 'Xiaomi', name: 'Xiaomi Redmi Note 10' }, |
|
|
{ model: '2201117TG', manufacturer: 'Xiaomi', name: 'Xiaomi 11T' }, |
|
|
{ model: 'CPH2121', manufacturer: 'OPPO', name: 'OPPO Reno5' }, |
|
|
{ model: 'RMX3085', manufacturer: 'realme', name: 'realme 8 Pro' }, |
|
|
{ model: 'itel S665L', manufacturer: 'ITEL', name: 'itel S665L' }, |
|
|
{ model: 'TECNO KE5', manufacturer: 'TECNO', name: 'TECNO Spark 7' } |
|
|
]; |
|
|
|
|
|
const versions = ['13.36.2', '13.35.0', '13.34.1', '13.33.0', '13.32.1']; |
|
|
const androidVersions = ['11', '12', '13']; |
|
|
|
|
|
const device = devices[Math.floor(Math.random() * devices.length)]; |
|
|
const version = versions[Math.floor(Math.random() * versions.length)]; |
|
|
const androidVer = androidVersions[Math.floor(Math.random() * androidVersions.length)]; |
|
|
const advertisingId = crypto.randomUUID(); |
|
|
const hardwareId = crypto.randomBytes(8).toString('hex'); |
|
|
const installId = crypto.randomBytes(16).toString('hex'); |
|
|
|
|
|
return { |
|
|
'User-Agent': `Pinterest for Android/${version} (${device.model}; ${androidVer})`, |
|
|
'accept-language': 'id-ID', |
|
|
'x-pinterest-advertising-id': advertisingId, |
|
|
'x-pinterest-app-type-detailed': '3', |
|
|
'x-pinterest-device': device.model, |
|
|
'x-pinterest-device-hardwareid': hardwareId, |
|
|
'x-pinterest-device-manufacturer': device.manufacturer, |
|
|
'x-pinterest-installid': installId, |
|
|
'x-pinterest-webview-supported': 'false', |
|
|
'x-pinterest-appstate': 'active', |
|
|
'x-node-id': 'true', |
|
|
'authorization': `Bearer ${this.authToken}` |
|
|
}; |
|
|
} |
|
|
|
|
|
async searchByImage(imageUrl, pageSize = 12) { |
|
|
const data = new FormData(); |
|
|
data.append('camera_type', '0'); |
|
|
data.append('source_type', '1'); |
|
|
data.append('video_autoplay_disabled', '0'); |
|
|
data.append('fields', this.getFields()); |
|
|
data.append('page_size', pageSize.toString()); |
|
|
data.append('image_url', imageUrl); |
|
|
|
|
|
const headers = this.getRandomHeaders(); |
|
|
const response = await axios.post( |
|
|
'https://api.pinterest.com/v3/visual_search/lens/search/', |
|
|
data, |
|
|
{ headers: { ...headers, ...data.getHeaders() } } |
|
|
); |
|
|
|
|
|
return this.parseResults(response.data); |
|
|
} |
|
|
|
|
|
async getMoreResults(bookmark, url, pageSize = 12) { |
|
|
const params = new URLSearchParams({ |
|
|
bookmark, |
|
|
camera_type: '0', |
|
|
source_type: '1', |
|
|
video_autoplay_disabled: '0', |
|
|
fields: this.getFields(), |
|
|
url, |
|
|
page_size: pageSize.toString(), |
|
|
view_type: '119', |
|
|
view_parameter: '3064' |
|
|
}); |
|
|
|
|
|
const headers = this.getRandomHeaders(); |
|
|
const response = await axios.get( |
|
|
`https://api.pinterest.com/v3/visual_search/lens/search/?${params}`, |
|
|
{ headers } |
|
|
); |
|
|
|
|
|
return this.parseResults(response.data); |
|
|
} |
|
|
|
|
|
async scrapeAll(imageUrl, maxPages = 5) { |
|
|
const allResults = []; |
|
|
|
|
|
const firstPage = await this.searchByImage(imageUrl); |
|
|
allResults.push(...firstPage.pins); |
|
|
|
|
|
let bookmark = firstPage.bookmark; |
|
|
let url = firstPage.url; |
|
|
let page = 2; |
|
|
|
|
|
while (bookmark && page <= maxPages) { |
|
|
const nextPage = await this.getMoreResults(bookmark, url); |
|
|
allResults.push(...nextPage.pins); |
|
|
bookmark = nextPage.bookmark; |
|
|
page++; |
|
|
await this.delay(100); |
|
|
} |
|
|
|
|
|
return { |
|
|
total: allResults.length, |
|
|
pins: allResults, |
|
|
visualObjects: firstPage.visualObjects, |
|
|
searchIdentifier: firstPage.searchIdentifier |
|
|
}; |
|
|
} |
|
|
|
|
|
parseResults(response) { |
|
|
const pins = response.data.map(pin => ({ |
|
|
id: pin.id, |
|
|
title: pin.title || '', |
|
|
description: pin.description || '', |
|
|
imageUrl: pin.images?.['736x']?.url || pin.images?.originals?.url, |
|
|
thumbnailUrl: pin.images?.['236x']?.url, |
|
|
dominantColor: pin.dominant_color, |
|
|
creator: { |
|
|
id: pin.pinner?.id, |
|
|
username: pin.pinner?.username, |
|
|
fullName: pin.pinner?.full_name, |
|
|
imageUrl: pin.pinner?.image_medium_url |
|
|
}, |
|
|
board: { |
|
|
id: pin.board?.id, |
|
|
name: pin.board?.name, |
|
|
url: pin.board?.url |
|
|
}, |
|
|
stats: { |
|
|
saves: pin.aggregated_pin_data?.aggregated_stats?.saves || 0, |
|
|
comments: pin.comment_count || 0 |
|
|
}, |
|
|
createdAt: pin.created_at, |
|
|
isVideo: pin.is_video || false, |
|
|
link: pin.link, |
|
|
domain: pin.domain |
|
|
})); |
|
|
|
|
|
return { |
|
|
pins, |
|
|
bookmark: response.bookmark, |
|
|
url: response.url, |
|
|
visualObjects: response.visual_objects, |
|
|
searchIdentifier: response.search_identifier |
|
|
}; |
|
|
} |
|
|
|
|
|
getFields() { |
|
|
return 'pin.{id,title,description,images[736x,236x],dominant_color,pinner(),board(),aggregated_pin_data(),comment_count,created_at,is_video,link,domain},user.{id,username,full_name,image_medium_url},board.{id,name,url},aggregatedpindata.{aggregated_stats}'; |
|
|
} |
|
|
|
|
|
delay(ms) { |
|
|
return new Promise(resolve => setTimeout(resolve, ms)); |
|
|
} |
|
|
} |
|
|
|
|
|
const handler = async (req, res) => { |
|
|
try { |
|
|
const { imageUrl, token, maxPages = 5, key } = req.query; |
|
|
|
|
|
if (!imageUrl) { |
|
|
return res.status(400).json({ |
|
|
success: false, |
|
|
error: 'Missing required parameter: imageUrl' |
|
|
}); |
|
|
} |
|
|
|
|
|
if (!token) { |
|
|
return res.status(400).json({ |
|
|
success: false, |
|
|
error: 'Missing required parameter: token' |
|
|
}); |
|
|
} |
|
|
|
|
|
const scraper = new PinterestLensScraper(token); |
|
|
const results = await scraper.scrapeAll(imageUrl, parseInt(maxPages)); |
|
|
|
|
|
res.json({ |
|
|
author: 'siputzx', |
|
|
success: true, |
|
|
data: results |
|
|
}); |
|
|
|
|
|
} catch (error) { |
|
|
res.status(500).json({ |
|
|
success: false, |
|
|
error: error.message |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
module.exports = { |
|
|
name: 'Pinterest Lens Scraper', |
|
|
description: 'Scrape Pinterest pins using image search with lens', |
|
|
type: 'GET', |
|
|
routes: ['api/tools/pinlens'], |
|
|
tags: ['tools', 'pinterest', 'image-search'], |
|
|
main: ['tools', 'Search'], |
|
|
parameters: ['imageUrl', 'token', 'maxPages', 'key'], |
|
|
enabled: true, |
|
|
limit: 10, |
|
|
handler |
|
|
}; |