RSSHub/lib/routes/bilibili/dynamic.ts

356 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ? `<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, 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) => `<img src="${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 = `合集地址:<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>`;
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}`;
text = `直播间地址:<a href=${url}>${url}</a>`;
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,
`<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 (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 = [description, originDescription, urlText, originUrlText, getIframe(data, disableEmbed), getIframe(origin, disableEmbed), getImgs(data), getImgs(origin)]
.filter(Boolean)
.map((e) => e?.trim())
.join('<br>');
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,
};
}