/** * Pinterest Search dan download * base: https://pinterest.com * Creator: ShanMolvyr * reupload/modif cantumkan sumber ini woii parah * * Note: cek https://snippet.vyr.my.id/shanmolvyr/pinterest/README.md * Sumber: https://whatsapp.com/channel/0029VbB4Kw8EFeXfeExaXc3Q */ const https = require("https"); const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); // ─── HTTP function httpsGet(url, headers = {}) { return new Promise((resolve, reject) => { const u = new URL(url); const req = https.get( { hostname: u.hostname, path: u.pathname + u.search, headers: { "User-Agent": "Mozilla/5.0 (Android 15; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0", Accept: "application/json, text/javascript, */*; q=0.01", "Accept-Language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7", "Accept-Encoding": "identity", "X-Requested-With": "XMLHttpRequest", "X-Pinterest-AppState": "active", "X-Pinterest-PWS-Handler": "www/search/[scope].js", ...headers, }, }, (res) => { let body = ""; res.on("data", (c) => (body += c)); res.on("end", () => resolve({ status: res.statusCode, body, headers: res.headers }) ); } ); req.on("error", reject); req.setTimeout(15000, () => { req.destroy(); reject(new Error("Timeout")); }); }); } function httpsGetStream(url, destPath) { return new Promise((resolve, reject) => { const u = new URL(url); const writer = fs.createWriteStream(destPath); const req = https.get( { hostname: u.hostname, path: u.pathname + u.search, headers: { "User-Agent": "Mozilla/5.0 (Android 15; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0", Referer: "https://www.pinterest.com/", }, }, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { writer.close(); return httpsGetStream(res.headers.location, destPath).then(resolve).catch(reject); } res.pipe(writer); writer.on("finish", () => resolve({ path: destPath, size: fs.statSync(destPath).size }) ); writer.on("error", reject); } ); req.on("error", reject); req.setTimeout(30000, () => { req.destroy(); reject(new Error("Download timeout")); }); }); } // ─── HELPERS const rand = (n) => crypto.randomBytes(n).toString("hex"); const appVer = () => rand(4).slice(0, 7); const ts = () => Date.now(); function buildSearchUrl(query, pageSize, bookmark) { const sourceUrl = `/search/pins/?q=${encodeURIComponent(query)}&rs=typed`; const options = { query, scope: "pins", page_size: pageSize, rs: "typed", redux_normalize_feed: true, appliedProductFilters: "---", auto_correction_disabled: false, static_feed: false, ...(bookmark ? { bookmarks: [bookmark] } : {}), }; const data = encodeURIComponent(JSON.stringify({ options, context: {} })); return ( `https://id.pinterest.com/resource/BaseSearchResource/get/` + `?source_url=${encodeURIComponent(sourceUrl)}&data=${data}&_=${ts()}` ); } function buildPinUrl(pinId) { const options = { id: pinId, field_set_key: "detailed" }; const data = encodeURIComponent(JSON.stringify({ options, context: {} })); return ( `https://id.pinterest.com/resource/PinResource/get/` + `?source_url=${encodeURIComponent(`/pin/${pinId}/`)}&data=${data}&_=${ts()}` ); } function apiHeaders(query) { const trace = rand(8); return { "X-APP-VERSION": appVer(), "X-B3-TraceId": trace, "X-B3-SpanId": rand(8), "X-B3-ParentSpanId": trace, "X-B3-Flags": "0", "screen-dpr": "2.857142857142857", "X-Pinterest-Source-Url": `/search/pins/?rs=typed&q=${encodeURIComponent(query || "")}`, Referer: `https://id.pinterest.com/search/pins/?rs=typed&q=${encodeURIComponent(query || "")}`, }; } // ─── PARSE function parsePin(p) { if (!p || !p.id) return null; const images = p.images || {}; const videos = p.videos?.video_list || null; let type = "image"; let mediaUrl = images["736x"]?.url || images.orig?.url || null; let sizes = { orig: images.orig || null, "736x": images["736x"] || null, "474x": images["474x"] || null, "236x": images["236x"] || null, }; if (videos && Object.keys(videos).length) { type = "video"; const sorted = Object.entries(videos).sort( (a, b) => (b[1]?.width || 0) - (a[1]?.width || 0) ); mediaUrl = sorted[0]?.[1]?.url || null; sizes = videos; } else if (mediaUrl?.includes(".gif") || p.is_gif) { type = "gif"; } return { id: p.id, type, title: p.title || p.grid_title || p.description?.slice(0, 80) || "", description: p.description || "", pinUrl: `https://www.pinterest.com/pin/${p.id}/`, mediaUrl, thumbnail: images["236x"]?.url || images["474x"]?.url || mediaUrl, sizes, link: p.link || null, board: p.board ? { id: p.board.id, name: p.board.name } : null, pinner: p.pinner ? { username: p.pinner.username, fullName: p.pinner.full_name, avatar: p.pinner.image_small_url || null, } : null, stats: { saves: p.save_count || p.repin_count || 0, comments: p.comment_count || 0, }, dominantColor: p.dominant_color || null, createdAt: p.created_at || null, }; } // ─── SEARCH async function search(query, opts = {}) { const { limit = 25, bookmark = null } = opts; const url = buildSearchUrl(query, limit, bookmark); const { status, body } = await httpsGet(url, apiHeaders(query)); if (status !== 200) throw new Error(`HTTP ${status}`); const json = JSON.parse(body); const rr = json?.resource_response; if (!rr || rr.status !== "success") { throw new Error(rr?.message || "Pinterest API engrorr"); } const pins = rr.data?.results || []; const nextBookmark = rr.data?.bookmark || null; return { query, total: pins.length, bookmark: nextBookmark, hasMore: !!nextBookmark, results: pins.map(parsePin).filter(Boolean), }; } async function searchAll(query, opts = {}) { const { total = 50 } = opts; let all = []; let bookmark = null; while (all.length < total) { const res = await search(query, { limit: Math.min(25, total - all.length), bookmark }); all = all.concat(res.results); bookmark = res.bookmark; if (!res.hasMore) break; await new Promise((r) => setTimeout(r, 800)); } return { query, total: all.length, results: all.slice(0, total) }; } // ─── DETAIL async function getPin(pinId) { const url = buildPinUrl(pinId); const { status, body } = await httpsGet(url, { ...apiHeaders(""), Referer: `https://id.pinterest.com/pin/${pinId}/`, "X-Pinterest-Source-Url": `/pin/${pinId}/`, }); if (status !== 200) throw new Error(`HTTP ${status}`); const json = JSON.parse(body); const pin = json?.resource_response?.data; if (!pin) throw new Error(`Pin ${pinId} gk ditemukan`); return parsePin(pin); } // ─── DOnglod async function downloadPin(pinId, outputDir = "./downloads") { if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); const pin = await getPin(pinId); if (!pin.mediaUrl) throw new Error(`gak ada mediaUrl untuk pin ${pinId}`); const url = pin.mediaUrl; let ext = "jpg"; if (pin.type === "gif" || url.includes(".gif")) ext = "gif"; else if (pin.type === "video" || url.includes(".mp4")) ext = "mp4"; else if (url.includes(".webp")) ext = "webp"; else if (url.includes(".png")) ext = "png"; const filename = `${pinId}.${ext}`; const outputPath = path.join(outputDir, filename); const dl = await httpsGetStream(url, outputPath); return { pinId, type: pin.type, title: pin.title, filename, ...dl, pin }; } async function downloadFromUrl(pinUrl, outputDir = "./downloads") { const match = pinUrl.match(/\/pin\/(\d+)/); if (!match) throw new Error("URL nya tidak valid"); return downloadPin(match[1], outputDir); } // ─── EXPORTS module.exports = { search, searchAll, getPin, downloadPin, downloadFromUrl }; // ─── CLI Nyah if (require.main === module) { (async () => { const [, , command, ...args] = process.argv; try { if (command === "search") { const [query, limitStr] = args; if (!query) throw new Error("Usage: node pinterest.js search [limit]"); const result = await search(query, { limit: parseInt(limitStr) || 25 }); console.log(JSON.stringify(result, null, 2)); } else if (command === "searchall") { const [query, totalStr] = args; if (!query) throw new Error("Usage: node pinterest.js searchall [total]"); const result = await searchAll(query, { total: parseInt(totalStr) || 50 }); console.log(JSON.stringify(result, null, 2)); } else if (command === "pin") { if (!args[0]) throw new Error("Usage: node pinterest.js pin "); console.log(JSON.stringify(await getPin(args[0]), null, 2)); } else if (command === "download") { const [target, outDir] = args; if (!target) throw new Error("Usage: node pinterest.js download [outputDir]"); const result = target.startsWith("http") ? await downloadFromUrl(target, outDir || "./downloads") : await downloadPin(target, outDir || "./downloads"); console.log(JSON.stringify(result, null, 2)); } else { console.error("Commands:"); console.error(" search [limit]"); console.error(" searchall [total]"); console.error(" pin "); console.error(" download [outputDir]"); } } catch (err) { console.error("[!] Error:", err.message); if (process.env.DEBUG) console.error(err.stack); process.exit(1); } })(); }