Skip to content

Commit

Permalink
api/processing: add support for xiaohongshu
Browse files Browse the repository at this point in the history
  • Loading branch information
wukko committed Jan 20, 2025
1 parent 63b2681 commit ed8f435
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 8 deletions.
2 changes: 2 additions & 0 deletions api/src/processing/match-action.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab
case "twitter":
case "snapchat":
case "bsky":
case "xiaohongshu":
params = { picker: r.picker };
break;

Expand Down Expand Up @@ -143,6 +144,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab
case "ok":
case "vk":
case "tiktok":
case "xiaohongshu":
params = { type: "proxy" };
break;

Expand Down
10 changes: 10 additions & 0 deletions api/src/processing/match.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import snapchat from "./services/snapchat.js";
import loom from "./services/loom.js";
import facebook from "./services/facebook.js";
import bluesky from "./services/bluesky.js";
import xiaohongshu from "./services/xiaohongshu.js";

let freebind;

Expand Down Expand Up @@ -239,6 +240,15 @@ export default async function({ host, patternMatch, params }) {
});
break;

case "xiaohongshu":
r = await xiaohongshu({
...patternMatch,
h265: params.tiktokH265,
isAudioOnly,
dispatcher,
});
break;

default:
return createResponse("error", {
code: "error.api.service.unsupported"
Expand Down
8 changes: 8 additions & 0 deletions api/src/processing/service-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@ export const services = {
subdomains: ["m"],
altDomains: ["vkvideo.ru", "vk.ru"],
},
xiaohongshu: {
patterns: [
"explore/:id?xsec_token=:token",
"discovery/item/:id?xsec_token=:token",
"a/:shareId"
],
altDomains: ["xhslink.com"],
},
youtube: {
patterns: [
"watch?v=:id",
Expand Down
4 changes: 4 additions & 0 deletions api/src/processing/service-patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ export const testers = {

"bsky": pattern =>
pattern.user?.length <= 128 && pattern.post?.length <= 128,

"xiaohongshu": pattern =>
pattern.id?.length <= 24 && pattern.token?.length <= 64
|| pattern.shareId?.length <= 12,
}
123 changes: 123 additions & 0 deletions api/src/processing/services/xiaohongshu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { extract, normalizeURL } from "../url.js";
import { genericUserAgent } from "../../config.js";
import { createStream } from "../../stream/manage.js";
import { getRedirectingURL } from "../../misc/utils.js";

const https = (url) => {
return url.replace(/^http:/i, 'https:');
}

export default async function ({ id, token, shareId, h265, isAudioOnly, dispatcher }) {
let noteId = id;
let xsecToken = token;

if (!noteId) {
const extractedURL = await getRedirectingURL(
`https://xhslink.com/a/${shareId}`,
dispatcher
);

if (extractedURL) {
const { patternMatch } = extract(normalizeURL(extractedURL));

if (patternMatch) {
noteId = patternMatch.id;
xsecToken = patternMatch.token;
}
}
}

if (!noteId || !xsecToken) return { error: "fetch.short_link" };

const res = await fetch(`https://www.xiaohongshu.com/explore/${noteId}?xsec_token=${xsecToken}`, {
headers: {
"user-agent": genericUserAgent,
},
dispatcher,
});

const html = await res.text();

let note;
try {
const initialState = html
.split('<script>window.__INITIAL_STATE__=')[1]
.split('</script>')[0]
.replace(/:undefined/g, ":null");

const data = JSON.parse(initialState);

const noteInfo = data?.note?.noteDetailMap;
if (!noteInfo) throw "no note detail map";

const currentNote = noteInfo[noteId];
if (!currentNote) throw "no current note in detail map";

note = currentNote.note;
} catch {
return { error: "fetch.empty" };
}

if (!note) return { error: "fetch.empty" };

const video = note.video;
const images = note.imageList;

const filenameBase = `xiaohongshu_${noteId}`;

if (video) {
const videoFilename = `${filenameBase}.mp4`;
const audioFilename = `${filenameBase}_audio`;

let videoURL;

if (h265 && !isAudioOnly && video.consumer?.originVideoKey) {
videoURL = `https://sns-video-bd.xhscdn.com/${video.consumer.originVideoKey}`;
}

if (!videoURL) {
const h264Streams = video.media?.stream?.h264;
if (!h264Streams) return { error: "fetch.empty" };

if (h264Streams.length > 1) {
videoURL = h264Streams.reduce((a, b) => Number(a?.videoBitrate) > Number(b?.videoBitrate) ? a : b).masterUrl;
} else {
videoURL = h264Streams[0].masterUrl;
}
}

if (!videoURL) return { error: "fetch.empty" };

return {
urls: https(videoURL),
filename: videoFilename,
audioFilename: audioFilename,
}
}

if (!images || images.length === 0) {
return { error: "fetch.empty" };
}

if (images.length === 1) {
return {
isPhoto: true,
urls: https(images[0].urlDefault),
filename: `${filenameBase}.jpg`,
}
}

const picker = images.map((image, i) => {
return {
type: "photo",
url: createStream({
service: "xiaohongshu",
type: "proxy",
url: https(image.urlDefault),
filename: `${filenameBase}_${i + 1}.jpg`,
})
}
});

return { picker };
}
26 changes: 18 additions & 8 deletions api/src/processing/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,14 @@ function aliasURL(url) {
url.hostname = 'vk.com';
}
break;

case "xhslink":
if (url.hostname === 'xhslink.com' && parts.length === 3) {
url = new URL(`https://www.xiaohongshu.com/a/${parts[2]}`);
}
}

return url
return url;
}

function cleanURL(url) {
Expand All @@ -114,36 +119,41 @@ function cleanURL(url) {
break;
case "vk":
if (url.pathname.includes('/clip') && url.searchParams.get('z')) {
limitQuery('z')
limitQuery('z');
}
break;
case "youtube":
if (url.searchParams.get('v')) {
limitQuery('v')
limitQuery('v');
}
break;
case "rutube":
if (url.searchParams.get('p')) {
limitQuery('p')
limitQuery('p');
}
break;
case "twitter":
if (url.searchParams.get('post_id')) {
limitQuery('post_id')
limitQuery('post_id');
}
break;
case "xiaohongshu":
if (url.searchParams.get('xsec_token')) {
limitQuery('xsec_token');
}
break;
}

if (stripQuery) {
url.search = ''
url.search = '';
}

url.username = url.password = url.port = url.hash = ''
url.username = url.password = url.port = url.hash = '';

if (url.pathname.endsWith('/'))
url.pathname = url.pathname.slice(0, -1);

return url
return url;
}

function getHostIfValid(url) {
Expand Down

0 comments on commit ed8f435

Please sign in to comment.