import { Route } from '@/types'; import cache from '@/utils/cache'; import got from '@/utils/got'; import JSONbig from 'json-bigint'; import utils 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'; export const route: Route = { path: '/user/dynamic/:uid/:routeParams?', categories: ['social-media'], example: '/bilibili/user/dynamic/2267573', parameters: { uid: '用户 id, 可在 UP 主主页中找到', routeParams: '额外参数;请参阅以下说明和表格' }, 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: true, supportBT: false, supportPodcast: false, supportScihub: false, }, radar: [ { source: ['space.bilibili.com/:uid'], target: '/user/dynamic/:uid', }, ], name: 'UP 主动态', maintainers: ['DIYgod', 'zytomorrow', 'CaoMeiYouRen', 'JimenezLi'], handler, description: `| 键 | 含义 | 接受的值 | 默认值 | | ------------ | --------------------------------- | -------------- | ------ | | showEmoji | 显示或隐藏表情图片 | 0/1/true/false | false | | disableEmbed | 关闭内嵌视频 | 0/1/true/false | false | | useAvid | 视频链接使用 AV 号 (默认为 BV 号) | 0/1/true/false | false | | directLink | 使用内容直链 | 0/1/true/false | false | 用例:\`/bilibili/user/dynamic/2267573/showEmoji=1&disableEmbed=1&useAvid=1\` :::tip 动态的专栏显示全文 动态的专栏显示全文请使用通用参数里的 \`mode=fulltext\` 举例: bilibili 专栏全文输出 /bilibili/user/dynamic/2267573/?mode=fulltext :::`, }; 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 ? `
//转发自: ${major.common.desc}` : major.common.desc; return desc; } // 转发的直播间 if (major?.live) { return `${major.live?.desc_first}
${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, disableEmbed: boolean = false) => { if (disableEmbed) { return ''; } const aid = data?.module_dynamic?.major?.archive?.aid; const bvid = data?.module_dynamic?.major?.archive?.bvid; if (!aid) { return ''; } return utils.iframe(aid, null, 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.map((url) => ``).join(''); }; const getUrl = (item?: Item2, useAvid = false) => { const data = item?.modules; if (!data) { return null; } let url = ''; let text = ''; 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 = `合集地址:${url}`; break; case 'MAJOR_TYPE_ARTICLE': url = `https://www.bilibili.com/read/cv${major?.article?.id}`; text = `专栏地址:${url}`; 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 = `视频地址:${url}`; break; } case 'MAJOR_TYPE_COMMON': url = major?.common?.jump_url || ''; text = `地址:${url}`; break; case 'MAJOR_TYPE_OPUS': if (item?.type === 'DYNAMIC_TYPE_ARTICLE') { url = `https:${major?.opus?.jump_url}`; text = `专栏地址:${url}`; } else if (item?.type === 'DYNAMIC_TYPE_DRAW') { url = `https:${major?.opus?.jump_url}`; text = `图文地址:${url}`; } 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 = `剧集地址:${url}`; break; } case 'MAJOR_TYPE_COURSES': url = `https://www.bilibili.com/cheese/play/ss${major?.courses?.id}`; text = `课程地址:${url}`; break; case 'MAJOR_TYPE_MUSIC': url = `https://www.bilibili.com/audio/au${major?.music?.id}`; text = `音频地址:${url}`; break; case 'MAJOR_TYPE_LIVE': url = `https://live.bilibili.com/${major?.live?.id}`; text = `直播间地址:${url}`; 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}`; text = `直播间地址:${url}`; break; } default: return null; } return { url, text, }; }; 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 disableEmbed = fallback(undefined, queryToBoolean(routeParams.disableEmbed), false); const displayArticle = ctx.req.query('mode') === 'fulltext'; const useAvid = fallback(undefined, queryToBoolean(routeParams.useAvid), false); const directLink = fallback(undefined, queryToBoolean(routeParams.directLink), false); const cookie = await cacheIn.getCookie(); const response = await got({ method: 'get', url: `https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space`, searchParams: { host_mid: uid, platform: 'web', features: 'itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote', }, headers: { Referer: `https://space.bilibili.com/${uid}/`, Cookie: cookie, }, }); const body = JSONbig.parse(response.body); if (body?.code === -352) { cacheIn.clearCookie(); throw new Error('The cookie has expired, please try again.'); } 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.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}`; } let description = getDes(data) || ''; const title = getTitle(data) || description; // 没有 title 的时候使用 desc 填充 // emoji if (data.module_dynamic?.desc?.rich_text_nodes?.length && showEmoji) { const nodes = data.module_dynamic?.desc?.rich_text_nodes; for (const node of nodes) { if (node?.emoji) { const emoji = node.emoji; description = description.replaceAll( emoji.text, `${emoji.text}` ); } } } 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 += `
${originDes}`; } // 换行处理 description = description.replaceAll('\r\n', '
').replaceAll('\n', '
'); originDescription = originDescription.replaceAll('\r\n', '
').replaceAll('\n', '
'); const descriptions = [description, originDescription, urlText, originUrlText, getIframe(data, disableEmbed), getIframe(origin, disableEmbed), getImgs(data), getImgs(origin)] .filter(Boolean) .map((e) => e?.trim()) .join('
'); return { title, description: descriptions, pubDate: data.module_author?.pub_ts ? parseDate(data.module_author.pub_ts, 'X') : undefined, link, author, }; }) ); return { title: `${author} 的 bilibili 动态`, link: `https://space.bilibili.com/${uid}/dynamic`, description: `${author} 的 bilibili 动态`, image: face, logo: face, icon: face, item: rssItems, }; }