Skip to content

Commit

Permalink
feat(route): 添加小红书 LivePhoto 视频支持 (DIYgod#17760)
Browse files Browse the repository at this point in the history
  • Loading branch information
ddddd993 authored Dec 2, 2024
1 parent 4c09251 commit 784c69f
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 8 deletions.
13 changes: 10 additions & 3 deletions lib/routes/xiaohongshu/user.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -45,20 +46,26 @@ 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;

if (cookie && category === 'notes') {
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,
Expand Down
70 changes: 65 additions & 5 deletions lib/routes/xiaohongshu/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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),
Expand All @@ -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', '<br>');
const pubDate = new Date(note.time);
const description = `${images.map((image) => `<img src="${image}">`).join('')}<br>${title}<br>${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 = `<video controls ${posterUrl ? `poster="${posterUrl}"` : ''}>
${videoUrls.map((url) => `<source src="${url}" type="video/mp4">`).join('\n')}
</video><br>`;
}
} 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 `<video controls poster="${image.urlDefault}">
${videoUrls.map((url) => `<source src="${url}" type="video/mp4">`).join('\n')}
</video>`;
}
}
return `<img src="${image.urlDefault}">`;
})
.join('<br>');
}

const description = `${mediaContent}<br>${title}<br>${desc}`;
return {
title,
description,
Expand Down

0 comments on commit 784c69f

Please sign in to comment.