From 3f785e7cbefd51589fb8eadd9f76cf0c68c7562d Mon Sep 17 00:00:00 2001 From: KwiatekMiki Date: Sat, 12 Jul 2025 15:10:08 +0200 Subject: [PATCH] api: support new xiaohongshu links, add fallbacks to getRedirectingURL closes #1394 Co-authored-by: wukko --- api/src/misc/utils.js | 32 ++++++++++++++++------ api/src/processing/service-config.js | 2 +- api/src/processing/service-patterns.js | 2 +- api/src/processing/services/xiaohongshu.js | 4 +-- api/src/processing/url.js | 2 +- api/src/util/tests/xiaohongshu.json | 8 +++--- 6 files changed, 33 insertions(+), 17 deletions(-) diff --git a/api/src/misc/utils.js b/api/src/misc/utils.js index 1cde3cdc..82fe40a7 100644 --- a/api/src/misc/utils.js +++ b/api/src/misc/utils.js @@ -1,4 +1,4 @@ -import { request } from 'undici'; +import { request } from "undici"; const redirectStatuses = new Set([301, 302, 303, 307, 308]); export async function getRedirectingURL(url, dispatcher, headers) { @@ -8,18 +8,34 @@ export async function getRedirectingURL(url, dispatcher, headers) { headers, redirect: 'manual' }; + const getParams = { + ...params, + method: 'GET', + }; - let location = await request(url, params).then(r => { + const callback = (r) => { if (redirectStatuses.has(r.statusCode) && r.headers['location']) { return r.headers['location']; } - }).catch(() => null); + } - location ??= await fetch(url, params).then(r => { - if (redirectStatuses.has(r.status) && r.headers.has('location')) { - return r.headers.get('location'); - } - }).catch(() => null); + /* + try request() with HEAD & GET, + then do the same with fetch + (fetch is required for shortened reddit links) + */ + + let location = await request(url, params) + .then(callback).catch(() => null); + + location ??= await request(url, getParams) + .then(callback).catch(() => null); + + location ??= await fetch(url, params) + .then(callback).catch(() => null); + + location ??= await fetch(url, getParams) + .then(callback).catch(() => null); return location; } diff --git a/api/src/processing/service-config.js b/api/src/processing/service-config.js index 906c23da..c21a4409 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -207,7 +207,7 @@ export const services = { patterns: [ "explore/:id?xsec_token=:token", "discovery/item/:id?xsec_token=:token", - "a/:shareId" + ":shareType/:shareId", ], altDomains: ["xhslink.com"], }, diff --git a/api/src/processing/service-patterns.js b/api/src/processing/service-patterns.js index e68a20be..4ae138c2 100644 --- a/api/src/processing/service-patterns.js +++ b/api/src/processing/service-patterns.js @@ -78,7 +78,7 @@ export const testers = { "xiaohongshu": pattern => pattern.id?.length <= 24 && pattern.token?.length <= 64 - || pattern.shareId?.length <= 24, + || pattern.shareId?.length <= 24 && pattern.shareType?.length === 1, "newgrounds": pattern => pattern.id?.length <= 12 || pattern.audioId?.length <= 12, diff --git a/api/src/processing/services/xiaohongshu.js b/api/src/processing/services/xiaohongshu.js index 06de21aa..f9cbf680 100644 --- a/api/src/processing/services/xiaohongshu.js +++ b/api/src/processing/services/xiaohongshu.js @@ -6,13 +6,13 @@ const https = (url) => { return url.replace(/^http:/i, 'https:'); } -export default async function ({ id, token, shareId, h265, isAudioOnly, dispatcher }) { +export default async function ({ id, token, shareType, shareId, h265, isAudioOnly, dispatcher }) { let noteId = id; let xsecToken = token; if (!noteId) { const patternMatch = await resolveRedirectingURL( - `https://xhslink.com/a/${shareId}`, + `https://xhslink.com/${shareType}/${shareId}`, dispatcher ); diff --git a/api/src/processing/url.js b/api/src/processing/url.js index dbbda1cd..5f93f1d0 100644 --- a/api/src/processing/url.js +++ b/api/src/processing/url.js @@ -99,7 +99,7 @@ function aliasURL(url) { case "xhslink": if (url.hostname === 'xhslink.com' && parts.length === 3) { - url = new URL(`https://www.xiaohongshu.com/a/${parts[2]}`); + url = new URL(`https://www.xiaohongshu.com/${parts[1]}/${parts[2]}`); } break; diff --git a/api/src/util/tests/xiaohongshu.json b/api/src/util/tests/xiaohongshu.json index a169cc23..ed523856 100644 --- a/api/src/util/tests/xiaohongshu.json +++ b/api/src/util/tests/xiaohongshu.json @@ -1,7 +1,7 @@ [ { "name": "video (might have expired)", - "url": "https://www.xiaohongshu.com/explore/67cc17a3000000000e00726a?xsec_token=CBSFRtbF57so920elY1kbIX4fE1nhrwlpGZs9m6pIFpwo=", + "url": "https://www.xiaohongshu.com/explore/685e63e1000000000b02ee3b?xsec_token=ABN8EQJCDMPcFX9RRggeIPSHLIJ8zkGceFDyBewLGUz30=", "canFail": true, "params": {}, "expected": { @@ -11,7 +11,7 @@ }, { "name": "picker with multiple live photos (might have expired)", - "url": "https://www.xiaohongshu.com/explore/67c691b4000000000d0159cc?xsec_token=CB8p1eyB5DiFkwlUpy1BTeVsI9oOve6ppNjuDzo8V8p5w=", + "url": "https://www.xiaohongshu.com/explore/687128a2000000001203d94c?xsec_token=CBlDi5QDXDWZu2uUmbUrpKwg8lEL3uC10mc59lGf43r9w=", "canFail": true, "params": {}, "expected": { @@ -21,7 +21,7 @@ }, { "name": "one photo (might have expired)", - "url": "https://www.xiaohongshu.com/explore/676e132d000000000b016f68?xsec_token=ABRv6LKzizOFeSaf2HnnBkdBqniB5Ak1fI8tMAHzO31jA", + "url": "https://www.xiaohongshu.com/explore/64726b99000000000800e115?xsec_token=ABoD3qPHqVZolCfS-J8UP9QQaPXZ6Z6PVyODrhaiUg27U=", "canFail": true, "params": {}, "expected": { @@ -31,7 +31,7 @@ }, { "name": "short link (might have expired)", - "url": "https://xhslink.com/a/czn4z6c1tic4", + "url": "https://xhslink.com/m/2wAnaTkLRc1", "canFail": true, "params": {}, "expected": {