UNPKG

rsshub

Version:
422 lines (396 loc) 17.7 kB
import { Route, ViewType } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; import JSONbig from 'json-bigint'; import utils, { getLiveUrl, getVideoUrl } from './utils'; import { parseDate } from '@/utils/parse-date'; import { fallback, queryToBoolean } from '@/utils/readable-social'; import cacheIn from './cache'; import { BilibiliWebDynamicResponse, Item2, Modules } from './api-interface'; import { parseDuration } from '@/utils/helpers'; export const route: Route = { path: '/user/dynamic/:uid/:routeParams?', categories: ['social-media'], view: ViewType.SocialMedia, example: '/bilibili/user/dynamic/2267573', parameters: { uid: '用户 id, 可在 UP 主主页中找到', routeParams: ` | 键 | 含义 | 接受的值 | 默认值 | | ---------- | --------------------------------- | -------------- | ------ | | showEmoji | 显示或隐藏表情图片 | 0/1/true/false | false | | embed | 默认开启内嵌视频 | 0/1/true/false | true | | useAvid | 视频链接使用 AV 号 (默认为 BV 号) | 0/1/true/false | false | | directLink | 使用内容直链 | 0/1/true/false | false | | hideGoods | 隐藏带货动态 | 0/1/true/false | false | | offset | 偏移状态 | string | "" | 用例:\`/bilibili/user/dynamic/2267573/showEmoji=1&embed=0&useAvid=1\``, }, features: { requireConfig: [ { name: 'BILIBILI_COOKIE_*', optional: true, description: `如果没有此配置,那么必须开启 puppeteer 支持;BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid,如 \`BILIBILI_COOKIE_2267573\`,获取方式: 1. 打开 [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8) 2. 打开控制台,切换到 Network 面板,刷新 3. 点击 dynamic_new 请求,找到 Cookie 4. 视频和专栏,UP 主粉丝及关注只要求 \`SESSDATA\` 字段,动态需复制整段 Cookie`, }, ], requirePuppeteer: false, antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, }, radar: [ { source: ['space.bilibili.com/:uid'], target: '/user/dynamic/:uid', }, ], name: 'UP 主动态', maintainers: ['DIYgod', 'zytomorrow', 'CaoMeiYouRen', 'JimenezLi'], handler, }; const getTitle = (data: Modules): string => { const major = data.module_dynamic?.major; if (!major) { return ''; } if (major.none) { return major.none.tips; } if (major.courses) { return `${major.courses?.title} - ${major.courses?.sub_title}`; } if (major.live_rcmd?.content) { // 正在直播的动态 return JSON.parse(major.live_rcmd.content)?.live_play_info?.title; } const type = major.type.replace('MAJOR_TYPE_', '').toLowerCase(); return major[type]?.title; }; const getDes = (data: Modules): string => { let desc = ''; if (data.module_dynamic?.desc?.text) { desc += data.module_dynamic.desc.text; } const major = data.module_dynamic?.major; // 普通转发 if (!major) { return desc; } // 普通分享 if (major?.common?.desc) { desc += desc ? `<br>//转发自: ${major.common.desc}` : major.common.desc; return desc; } // 转发的直播间 if (major?.live) { return `${major.live?.desc_first}<br>${major.live?.desc_second}`; } // 正在直播的动态 if (major.live_rcmd?.content) { const live_play_info = JSON.parse(major.live_rcmd.content)?.live_play_info; return `${live_play_info?.area_name}·${live_play_info?.watched_show?.text_large}`; } // 图文动态 if (major?.opus) { return major?.opus?.summary?.text; } const type = major.type.replace('MAJOR_TYPE_', '').toLowerCase(); return major[type]?.desc; }; const getOriginTitle = (data?: Modules) => data && getTitle(data); const getOriginDes = (data?: Modules) => data && getDes(data); const getOriginName = (data?: Modules) => data?.module_author?.name; const getIframe = (data?: Modules, embed: boolean = true) => { if (!embed) { return ''; } const aid = data?.module_dynamic?.major?.archive?.aid; const bvid = data?.module_dynamic?.major?.archive?.bvid; if (aid === undefined && bvid === undefined) { return ''; } // 不通过 utils.renderUGCDescription 渲染 img/description 以兼容其他格式的动态 return utils.renderUGCDescription(embed, '', '', aid, undefined, bvid); }; const getImgs = (data?: Modules) => { const imgUrls: string[] = []; const major = data?.module_dynamic?.major; if (!major) { return ''; } // 动态图片 if (major.opus?.pics?.length) { imgUrls.push(...major.opus.pics.map((e) => e.url)); } // 专栏封面 if (major.article?.covers?.length) { imgUrls.push(...major.article.covers); } // 相簿 if (major.draw?.items?.length) { imgUrls.push(...major.draw.items.map((e) => e.src)); } // 正在直播的动态 if (major.live_rcmd?.content) { imgUrls.push(JSON.parse(major.live_rcmd.content)?.live_play_info?.cover); } const type = major.type.replace('MAJOR_TYPE_', '').toLowerCase(); if (major[type]?.cover) { imgUrls.push(major[type].cover); } return imgUrls .filter(Boolean) .map((url) => `<img src="${url}">`) .join(''); }; const getUrl = (item?: Item2, useAvid = false) => { const data = item?.modules; if (!data) { return null; } let url = ''; let text = ''; let videoPageUrl; const major = data.module_dynamic?.major; if (!major) { return null; } switch (major?.type) { case 'MAJOR_TYPE_UGC_SEASON': url = major?.ugc_season?.jump_url || ''; text = `合集地址:<a href=${url}>${url}</a>`; break; case 'MAJOR_TYPE_ARTICLE': url = `https://www.bilibili.com/read/cv${major?.article?.id}`; text = `专栏地址:<a href=${url}>${url}</a>`; break; case 'MAJOR_TYPE_ARCHIVE': { const archive = major?.archive; const id = useAvid ? `av${archive?.aid}` : archive?.bvid; url = `https://www.bilibili.com/video/${id}`; text = `视频地址:<a href=${url}>${url}</a>`; videoPageUrl = getVideoUrl(archive?.bvid); break; } case 'MAJOR_TYPE_COMMON': url = major?.common?.jump_url || ''; text = `地址:<a href=${url}>${url}</a>`; break; case 'MAJOR_TYPE_OPUS': if (item?.type === 'DYNAMIC_TYPE_ARTICLE') { url = `https:${major?.opus?.jump_url}`; text = `专栏地址:<a href=${url}>${url}</a>`; } else if (item?.type === 'DYNAMIC_TYPE_DRAW') { url = `https:${major?.opus?.jump_url}`; text = `图文地址:<a href=${url}>${url}</a>`; } break; case 'MAJOR_TYPE_PGC': { const pgc = major?.pgc; url = `https://www.bilibili.com/bangumi/play/ep${pgc?.epid}&season_id=${pgc?.season_id}`; text = `剧集地址:<a href=${url}>${url}</a>`; break; } case 'MAJOR_TYPE_COURSES': url = `https://www.bilibili.com/cheese/play/ss${major?.courses?.id}`; text = `课程地址:<a href=${url}>${url}</a>`; break; case 'MAJOR_TYPE_MUSIC': url = `https://www.bilibili.com/audio/au${major?.music?.id}`; text = `音频地址:<a href=${url}>${url}</a>`; break; case 'MAJOR_TYPE_LIVE': url = `https://live.bilibili.com/${major?.live?.id}`; text = `直播间地址:<a href=${url}>${url}</a>`; break; case 'MAJOR_TYPE_LIVE_RCMD': { const live_play_info = JSON.parse(major.live_rcmd?.content || '{}')?.live_play_info; url = `https://live.bilibili.com/${live_play_info?.room_id}`; videoPageUrl = getLiveUrl(live_play_info?.room_id); text = `直播间地址:<a href=${url}>${url}</a>`; break; } default: return null; } return { url, text, videoPageUrl, }; }; async function handler(ctx) { const uid = ctx.req.param('uid'); const routeParams = Object.fromEntries(new URLSearchParams(ctx.req.param('routeParams'))); const showEmoji = fallback(undefined, queryToBoolean(routeParams.showEmoji), false); const embed = fallback(undefined, queryToBoolean(routeParams.embed), false); const displayArticle = ctx.req.query('mode') === 'fulltext'; const offset = fallback(undefined, routeParams.offset, ''); const useAvid = fallback(undefined, queryToBoolean(routeParams.useAvid), false); const directLink = fallback(undefined, queryToBoolean(routeParams.directLink), false); const hideGoods = fallback(undefined, queryToBoolean(routeParams.hideGoods), false); const getDynamic = async (cookie: string) => { const params = utils.addDmVerifyInfo(`offset=${offset}&host_mid=${uid}&platform=web&features=itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote`, utils.getDmImgList()); const response = await got(`https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?${params}`, { headers: { Referer: `https://space.bilibili.com/${uid}/`, Cookie: cookie, }, }); const body = JSONbig.parse(response.body); return body; }; let body: BilibiliWebDynamicResponse; const cookie = (await cacheIn.getCookie()) as string; body = await getDynamic(cookie); if (body?.code === -352) { const cookie = (await cacheIn.getCookie(true)) as string; body = await getDynamic(cookie); if (body?.code === -352) { throw new Error('遇到源站风控校验,请稍后再试'); } } const items = (body as BilibiliWebDynamicResponse)?.data?.items; const usernameAndFace = await cacheIn.getUsernameAndFaceFromUID(uid); const author = usernameAndFace[0] ?? items[0]?.modules?.module_author?.name; const face = usernameAndFace[1] ?? items[0]?.modules?.module_author?.face; cache.set(`bili-username-from-uid-${uid}`, author); cache.set(`bili-userface-from-uid-${uid}`, face); const rssItems = await Promise.all( items .filter((item) => { if (hideGoods) { return item.modules.module_dynamic?.additional?.type !== 'ADDITIONAL_TYPE_GOODS'; } return true; }) .map(async (item) => { // const parsed = JSONbig.parse(item.card); const data = item.modules; const origin = item?.orig?.modules; // link let link = ''; if (item.id_str) { link = `https://t.bilibili.com/${item.id_str}`; } const originalDescription = getDes(data) || ''; let description = originalDescription; const title = getTitle(data); const category: string[] = []; // emoji if (data.module_dynamic?.desc?.rich_text_nodes?.length) { const nodes = data.module_dynamic.desc.rich_text_nodes; for (const node of nodes) { // 处理 emoji 的情况 if (showEmoji && node?.emoji) { const emoji = node.emoji; description = description.replaceAll( emoji.text, `<img alt="${emoji.text}" src="${emoji.icon_url}" style="margin: -1px 1px 0px; display: inline-block; width: 20px; height: 20px; vertical-align: text-bottom;" title="" referrerpolicy="no-referrer">` ); } // 处理转发带图评论的情况 if (node?.pics?.length) { const { pics, text } = node; description = description.replaceAll( text, pics .map( (pic) => `<img alt="${text}" src="${pic.src}" style="margin: 0px 0px 0px; display: inline-block; width: ${pic.width}px; height: ${pic.height}px; vertical-align: text-bottom;" title="" referrerpolicy="no-referrer">` ) .join('<br>') ); } if (node?.type === 'RICH_TEXT_NODE_TYPE_TOPIC') { // 将话题作为 category category.push(node.text.match(/#(\S+)#/)?.[1] || ''); } } } if (data.module_dynamic?.major?.opus?.summary?.rich_text_nodes?.length) { const nodes = data.module_dynamic.major.opus.summary.rich_text_nodes; for (const node of nodes) { if (node?.type === 'RICH_TEXT_NODE_TYPE_TOPIC') { // 将话题作为 category category.push(node.text.match(/#(\S+)#/)?.[1] || ''); } } } if (data.module_dynamic?.topic?.name) { // 将话题作为 category category.push(data.module_dynamic.topic.name); } if (item.type === 'DYNAMIC_TYPE_ARTICLE' && displayArticle) { // 抓取专栏全文 const cvid = data.module_dynamic?.major?.opus?.jump_url?.match?.(/cv(\d+)/)?.[0]; if (cvid) { description = (await cacheIn.getArticleDataFromCvid(cvid, uid)).description || ''; } } const urlResult = getUrl(item, useAvid); const urlText = urlResult?.text; if (urlResult && directLink) { link = urlResult.url; } const originUrlResult = getUrl(item?.orig, useAvid); const originUrlText = originUrlResult?.text; if (originUrlResult && directLink) { link = originUrlResult.url; } let originDescription = ''; const originName = getOriginName(origin); const originTitle = getOriginTitle(origin); const originDes = getOriginDes(origin); if (originName) { originDescription += `//转发自: @${getOriginName(origin)}: `; } if (originTitle) { originDescription += originTitle; } if (originDes) { originDescription += `<br>${originDes}`; } // 换行处理 description = description.replaceAll('\r\n', '<br>').replaceAll('\n', '<br>'); originDescription = originDescription.replaceAll('\r\n', '<br>').replaceAll('\n', '<br>'); const descriptions = [title, description, getIframe(data, embed), getImgs(data), urlText, originDescription, getIframe(origin, embed), getImgs(origin), originUrlText] .map((e) => e?.trim()) .filter(Boolean) .join('<br>'); return { title: title || originalDescription, description: descriptions, pubDate: data.module_author?.pub_ts ? parseDate(data.module_author.pub_ts, 'X') : undefined, link, author, category: category.length ? [...new Set(category)] : undefined, attachments: urlResult?.videoPageUrl || originUrlResult?.videoPageUrl ? [ { url: urlResult?.videoPageUrl || originUrlResult?.videoPageUrl, mime_type: 'text/html', duration_in_seconds: data.module_dynamic?.major?.archive?.duration_text ? parseDuration(data.module_dynamic.major.archive.duration_text) : undefined, }, ] : undefined, }; }) ); return { title: `${author} 的 bilibili 动态`, link: `https://space.bilibili.com/${uid}/dynamic`, description: `${author} 的 bilibili 动态`, image: face, logo: face, icon: face, item: rssItems, }; }