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,