From 784c69f555c8ec3527b5677ff8517e182f45272a Mon Sep 17 00:00:00 2001
From: ddddd993 <190154613+ddddd993@users.noreply.github.com>
Date: Mon, 2 Dec 2024 15:09:07 +0800
Subject: [PATCH] =?UTF-8?q?feat(route):=20=E6=B7=BB=E5=8A=A0=E5=B0=8F?=
=?UTF-8?q?=E7=BA=A2=E4=B9=A6=20LivePhoto=20=E8=A7=86=E9=A2=91=E6=94=AF?=
=?UTF-8?q?=E6=8C=81=20(#17760)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/routes/xiaohongshu/user.ts | 13 +++++--
lib/routes/xiaohongshu/util.ts | 70 +++++++++++++++++++++++++++++++---
2 files changed, 75 insertions(+), 8 deletions(-)
diff --git a/lib/routes/xiaohongshu/user.ts b/lib/routes/xiaohongshu/user.ts
index 89b82a97289f73..e97a226757883c 100644
--- a/lib/routes/xiaohongshu/user.ts
+++ b/lib/routes/xiaohongshu/user.ts
@@ -1,11 +1,12 @@
import { Route, ViewType } from '@/types';
import cache from '@/utils/cache';
+import querystring from 'querystring';
import { getUser, renderNotesFulltext, getUserWithCookie } from './util';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import { config } from '@/config';
-
+import { fallback, queryToBoolean } from '@/utils/readable-social';
export const route: Route = {
- path: '/user/:user_id/:category',
+ path: '/user/:user_id/:category/:routeParams?',
name: '用户笔记/收藏',
categories: ['social-media', 'popular'],
view: ViewType.Articles,
@@ -45,12 +46,18 @@ export const route: Route = {
],
default: 'notes',
},
+ routeParams: {
+ description: 'displayLivePhoto,`/user/:user_id/notes/displayLivePhoto=0`,不限时LivePhoto显示为图片,`/user/:user_id/notes/displayLivePhoto=1`,取值不为0时LivePhoto显示为视频',
+ default: '0',
+ },
},
};
async function handler(ctx) {
const userId = ctx.req.param('user_id');
const category = ctx.req.param('category');
+ const routeParams = querystring.parse(ctx.req.param('routeParams'));
+ const displayLivePhoto = !!fallback(undefined, queryToBoolean(routeParams.displayLivePhoto), false);
const url = `https://www.xiaohongshu.com/user/profile/${userId}`;
const cookie = config.xiaohongshu.cookie;
@@ -58,7 +65,7 @@ async function handler(ctx) {
try {
const urlNotePrefix = 'https://www.xiaohongshu.com/explore';
const user = await getUserWithCookie(url, cookie);
- const notes = await renderNotesFulltext(user.notes, urlNotePrefix);
+ const notes = await renderNotesFulltext(user.notes, urlNotePrefix, displayLivePhoto);
return {
title: `${user.userPageData.basicInfo.nickname} - 笔记 • 小红书 / RED`,
description: user.userPageData.basicInfo.desc,
diff --git a/lib/routes/xiaohongshu/util.ts b/lib/routes/xiaohongshu/util.ts
index 42947aa67fcdab..50bd213822ea84 100644
--- a/lib/routes/xiaohongshu/util.ts
+++ b/lib/routes/xiaohongshu/util.ts
@@ -118,7 +118,7 @@ const formatNote = (url, note) => ({
updated: parseDate(note.lastUpdateTime, 'x'),
});
-async function renderNotesFulltext(notes, urlPrex) {
+async function renderNotesFulltext(notes, urlPrex, displayLivePhoto) {
const data: Array<{
title: string;
link: string;
@@ -130,7 +130,7 @@ async function renderNotesFulltext(notes, urlPrex) {
const promises = notes.flatMap((note) =>
note.map(async ({ noteCard, id }) => {
const link = `${urlPrex}/${id}`;
- const { title, description, pubDate } = await getFullNote(link);
+ const { title, description, pubDate } = await getFullNote(link, displayLivePhoto);
return {
title,
link,
@@ -145,7 +145,7 @@ async function renderNotesFulltext(notes, urlPrex) {
return data;
}
-async function getFullNote(link) {
+async function getFullNote(link, displayLivePhoto) {
const data = (await cache.tryGet(link, async () => {
const res = await ofetch(link, {
headers: getHeaders(config.xiaohongshu.cookie),
@@ -154,14 +154,74 @@ async function getFullNote(link) {
const script = extractInitialState($);
const state = JSON.parse(script);
const note = state.note.noteDetailMap[state.note.firstNoteId].note;
- const images = note.imageList.map((image) => image.urlDefault);
const title = note.title;
let desc = note.desc;
desc = desc.replaceAll(/\[.*?\]/g, '');
desc = desc.replaceAll(/#(.*?)#/g, '#$1');
desc = desc.replaceAll('\n', '
');
const pubDate = new Date(note.time);
- const description = `${images.map((image) => ``).join('')}
${title}
${desc}`;
+
+ let mediaContent = '';
+ if (note.type === 'video') {
+ const originVideoKey = note.video?.consumer?.originVideoKey;
+ const videoUrls: string[] = [];
+
+ if (originVideoKey) {
+ videoUrls.push(`http://sns-video-al.xhscdn.com/${originVideoKey}`);
+ }
+
+ const streamTypes = ['av1', 'h264', 'h265', 'h266'];
+ for (const type of streamTypes) {
+ const streams = note.video?.media?.stream?.[type];
+ if (streams?.length > 0) {
+ const stream = streams[0];
+ if (stream.masterUrl) {
+ videoUrls.push(stream.masterUrl);
+ }
+ if (stream.backupUrls?.length) {
+ videoUrls.push(...stream.backupUrls);
+ }
+ }
+ }
+
+ const posterUrl = note.imageList?.[0]?.urlDefault;
+
+ if (videoUrls.length > 0) {
+ mediaContent = `
`;
+ }
+ } else {
+ mediaContent = note.imageList
+ .map((image) => {
+ if (image.livePhoto && displayLivePhoto) {
+ const videoUrls: string[] = [];
+
+ const streamTypes = ['av1', 'h264', 'h265', 'h266'];
+ for (const type of streamTypes) {
+ const streams = image.stream?.[type];
+ if (streams?.length > 0) {
+ if (streams[0].masterUrl) {
+ videoUrls.push(streams[0].masterUrl);
+ }
+ if (streams[0].backupUrls?.length) {
+ videoUrls.push(...streams[0].backupUrls);
+ }
+ }
+ }
+
+ if (videoUrls.length > 0) {
+ return ``;
+ }
+ }
+ return ``;
+ })
+ .join('
');
+ }
+
+ const description = `${mediaContent}
${title}
${desc}`;
return {
title,
description,