UNPKG

rsshub

Version:
222 lines (221 loc) 8.83 kB
import { t as rofetch } from "./ofetch-C3ts-Hud.mjs"; import { t as config } from "./config-CCmw1BNE.mjs"; import { t as logger } from "./logger-BKy98Y8B.mjs"; import { t as parseDate } from "./parse-date-Cr0wQjV_.mjs"; import { t as cache_default } from "./cache-BkqOokyU.mjs"; import { n as outPlaywright, t as getPlaywrightPage } from "./playwright-o20DiLNh.mjs"; import { t as CaptchaError } from "./captcha-BP6deQBe.mjs"; import { load } from "cheerio"; //#region lib/routes/xiaohongshu/util.ts const getHeaders = (cookie) => ({ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "Cache-Control": "no-cache", Connection: "keep-alive", Host: "www.xiaohongshu.com", Pragma: "no-cache", "Sec-Ch-Ua": "\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\"", "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": "\"Windows\"", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", ...cookie && { Cookie: cookie } }); async function fetchWithProxy(url, cookie) { const proxy = config.xiaohongshu.proxy; if (proxy) { const proxyUrl = `${proxy}?url=${encodeURIComponent(url)}`; logger.http(`Requesting ${url} via proxy`); return await rofetch(proxyUrl, { parseResponse: (txt) => txt }); } logger.http(`Requesting ${url}`); return await rofetch(url, { headers: getHeaders(cookie) }); } const getUser = (url, cache) => cache.tryGet(url, async () => { if (config.xiaohongshu.proxy) { const script = extractInitialState(load(await fetchWithProxy(url))); let { userPageData, notes } = JSON.parse(script).user; userPageData = userPageData._rawValue || userPageData; notes = notes._rawValue || notes; return { userPageData, notes, collect: "" }; } const { page, destroy } = await getPlaywrightPage(url, { onBeforeLoad: async (page) => { await page.route("**/*", (route) => { const request = route.request(); request.resourceType() === "document" || request.resourceType() === "script" || request.resourceType() === "xhr" || request.resourceType() === "other" ? route.continue() : route.abort(); }); } }); try { let collect = ""; logger.http(`Requesting ${url}`); await page.goto(url, { waitUntil: "domcontentloaded" }); try { await page.waitForSelector("div.reds-tab-item:nth-child(2), .fe-verify-box", { timeout: 3e3 }); } catch {} if (await page.$(".fe-verify-box")) throw new CaptchaError("小红书风控校验,请稍后再试"); const content = await page.content(); const initialState = JSON.parse(extractInitialState(load(content))); if (!await page.$(".lock-icon")) try { await page.click("div.reds-tab-item:nth-child(2)", { timeout: 3e3 }); collect = await (await page.waitForResponse((res) => { const req = res.request(); return req.url().includes("/api/sns/web/v2/note/collect/page") && req.method() === "GET" && req.resourceType() === "xhr"; }, { timeout: 5e3 })).json(); } catch {} let { userPageData, notes } = initialState.user; userPageData = userPageData._rawValue || userPageData; notes = notes._rawValue || notes; if (!userPageData.basicInfo) throw new Error(`小红书未返回用户数据,请稍后再试: ${JSON.stringify(userPageData.result)}`); return { userPageData, notes, collect }; } finally { await destroy(); } }, config.cache.routeExpire, false); const getBoard = (url, cache) => cache.tryGet(url, async () => { if (config.xiaohongshu.proxy) { const script = extractInitialSsrState(load(await fetchWithProxy(url))); return JSON.parse(script).Main; } const context = await outPlaywright(); try { const page = await context.newPage(); await page.route("**/*", (route) => { const request = route.request(); request.resourceType() === "document" || request.resourceType() === "script" || request.resourceType() === "xhr" ? route.continue() : route.abort(); }); logger.http(`Requesting ${url}`); await page.goto(url); await page.waitForSelector(".pc-container"); return (await page.evaluate(() => window.__INITIAL_SSR_STATE__)).Main; } finally { await context.close(); } }, config.cache.routeExpire, false); async function renderNotesFulltext(notes, urlPrex, displayLivePhoto) { const data = []; const promises = notes.flatMap((note) => note.map(async ({ noteCard, id }) => { const link = `${urlPrex}/${id}`; const guid = `${urlPrex}/${noteCard.noteId}`; const { title, description, pubDate, updated } = await getFullNote(link, displayLivePhoto); return { title, link, description, author: noteCard.user.nickName, guid, pubDate, updated }; })); data.push(...await Promise.all(promises)); return data; } async function getFullNote(link, displayLivePhoto) { return await cache_default.tryGet(link, async () => { const script = extractInitialState(load(await fetchWithProxy(link, config.xiaohongshu.cookie))); const state = JSON.parse(script); const note = state.note.noteDetailMap[state.note.firstNoteId].note; const title = note.title; let desc = note.desc; desc = desc.replaceAll(/\[.*?\]/g, ""); desc = desc.replaceAll(/#(.*?)#/g, "#$1"); desc = desc.replaceAll("\n", "<br>"); const pubDate = parseDate(note.time, "x"); const updated = parseDate(note.lastUpdateTime, "x"); let mediaContent = ""; if (note.type === "video") { const originVideoKey = note.video?.consumer?.originVideoKey; const videoUrls = []; if (originVideoKey) videoUrls.push(`http://sns-video-al.xhscdn.com/${originVideoKey}`); for (const type of [ "av1", "h264", "h265", "h266" ]) { 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 = []; for (const type of [ "av1", "h264", "h265", "h266" ]) { 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>${desc}`; return { title: title || note.desc, description, pubDate, updated }; }); } async function getUserWithCookie(url) { const cookie = config.xiaohongshu.cookie; const $ = load(await fetchWithProxy(url, cookie)); const paths = $("#userPostedFeeds > section > div > a.cover.ld.mask").map((i, item) => item.attributes[3].value); const script = extractInitialState($); const state = JSON.parse(script); let index = 0; for (const item of state.user.notes.flat()) { const path = paths[index]; if (path && path.includes("?")) item.id += path?.slice(path.indexOf("?")); index += 1; } return state.user; } function extractInitialState($) { let script = $("script:contains(\"window.__INITIAL_STATE__=\")").text(); script = script.slice(script.indexOf("window.__INITIAL_STATE__=") + 25); script = script.replaceAll("undefined", "null"); return script; } function extractInitialSsrState($) { const match = $("script:contains(\"window.__INITIAL_SSR_STATE__=\")").text().match(/window\.__INITIAL_SSR_STATE__\s*=\s*(\{[\s\S]*?\})\s*(?:;|$)/); if (match) return match[1].replaceAll("undefined", "null"); throw new Error("Cannot extract __INITIAL_SSR_STATE__"); } async function checkCookie() { const cookie = config.xiaohongshu.cookie; const res = await rofetch("https://edith.xiaohongshu.com/api/sns/web/v2/user/me", { headers: getHeaders(cookie) }); return res.code === 0 && !!res.data.user_id; } //#endregion export { renderNotesFulltext as a, getUserWithCookie as i, getBoard as n, getUser as r, checkCookie as t };