From 5dde49aeb256ae5be2233505e20682de25f008ab Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 4 May 2026 09:05:58 +0200 Subject: [PATCH] api/pinterest: fallback to the graphql api --- api/src/processing/services/pinterest.js | 62 +++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/api/src/processing/services/pinterest.js b/api/src/processing/services/pinterest.js index c21400e1..94a98fbb 100644 --- a/api/src/processing/services/pinterest.js +++ b/api/src/processing/services/pinterest.js @@ -1,10 +1,67 @@ import { genericUserAgent } from "../../config.js"; import { resolveRedirectingURL } from "../url.js"; +import { randomBytes } from 'node:crypto'; const videoRegex = /"url":"(https:\/\/v1\.pinimg\.com\/videos\/.*?)"/g; const imageRegex = /src="(https:\/\/i\.pinimg\.com\/.*\.(jpg|gif))"/g; const notFoundRegex = /"__typename"\s*:\s*"PinNotFound"/; +export async function fetchFromGraphQL(id) { + const csrf = randomBytes(16).toString("hex"); + + const responseData = await fetch("https://www.pinterest.com/_/graphql/", { + body: JSON.stringify({ + "queryHash": "a5ebb2b085f4c3f33f14ebb7da5ca572bcde8549c478fdccfbaddbbac4f01b92", + "variables": { + "pinId": id, + "isAuth": false, + "isDesktop": true, + "shouldPrefetchStoryPinFragment": false, + "shouldSkipImageViewerOnPageQuery": false, + "isUnauth": true + } + }), + headers: { + "Content-Type": "application/json", + "Cookie": `csrftoken=${csrf}`, + "User-Agent": genericUserAgent, + "X-Csrftoken": csrf, + }, + method: "POST", + }).then(r => r.json()); + + const pinResponse = responseData?.data?.v3GetPinQueryv2; + if (!pinResponse || pinResponse.__typename !== "PinResponse") { + return; + } + + + const pinData = pinResponse?.data; + let bestQuality = pinData["images_orig"]; + if (!bestQuality) { + for (const key of Object.keys(pinData)) { + if (!key.startsWith("images_")) continue; + const image = pinData[key]; + const size = Math.max(image.height, image.width); + image.size = size; + + if (!bestQuality || size > bestQuality?.size) { + bestQuality = image; + } + } + } + + const imageType = bestQuality.url?.endsWith(".gif") ? "gif" : "jpg"; + + if (bestQuality) { + return { + urls: bestQuality.url, + isPhoto: true, + filename: `pinterest_${id}.${imageType}` + } + } +} + export default async function(o) { let id = o.id; @@ -40,7 +97,7 @@ export default async function(o) { .map(([, link]) => link) .find(a => a.endsWith('.jpg') || a.endsWith('.gif')); - const imageType = imageLink.endsWith(".gif") ? "gif" : "jpg" + const imageType = imageLink?.endsWith(".gif") ? "gif" : "jpg" if (imageLink) return { urls: imageLink, @@ -48,5 +105,8 @@ export default async function(o) { filename: `pinterest_${id}.${imageType}` } + const graphResponse = await fetchFromGraphQL(id); + if (graphResponse) return graphResponse; + return { error: "fetch.empty" }; }