rsshub
Version:
Make RSS Great Again!
346 lines (337 loc) • 18.2 kB
JavaScript
import { t as config } from "./config-CCmw1BNE.mjs";
import "./types-DNj6Etbg.mjs";
import { t as cache_default } from "./cache-BkqOokyU.mjs";
import { t as got_default } from "./got-BTowW50G.mjs";
import { i as queryToInteger, n as queryToBoolean, t as fallback } from "./readable-social-DP5kGwF5.mjs";
import querystring from "node:querystring";
//#region lib/routes/douban/people/status.ts
const route = {
path: "/people/:userid/status/:routeParams?",
categories: ["social-media"],
view: 1,
example: "/douban/people/75118396/status",
parameters: {
userid: "整数型用户 id",
routeParams: "额外参数;见下"
},
name: "用户广播",
maintainers: ["alfredcai"],
handler,
description: `::: tip
- **目前只支持整数型 id**
- 字母型的 id,可以通过头像图片链接来找到其整数型 id,图片命名规则\`ul[userid]-*.jpg\`或\`u[userid]-*.jpg\`,即取文件名中间的数字
- 例如:用户 id: \`MovieL\`他的头像图片链接:\`https://img1.doubanio.com/icon/ul1128221-98.jpg\`他的整数型 id: \`1128221\`
:::
对于豆瓣用户广播内容,在 \`routeParams\` 参数中以 query string 格式设置如下选项可以控制输出的样式
| 键 | 含义 | 接受的值 | 默认值 |
| -------------------------- | -------------------------------------------------------------- | -------------- | ------ |
| readable | 是否开启细节排版可读性优化 | 0/1/true/false | false |
| authorNameBold | 是否加粗作者名字 | 0/1/true/false | false |
| showAuthorInTitle | 是否在标题处显示作者 | 0/1/true/false | true |
| showAuthorInDesc | 是否在正文处显示作者 | 0/1/true/false | false |
| showAuthorAvatarInDesc | 是否在正文处显示作者头像(若阅读器会提取正文图片,不建议开启) | 0/1/true/false | false |
| showEmojiForRetweet | 显示 “🔁” 取代 “Fw”(转发) | 0/1/true/false | false |
| showRetweetTextInTitle | 在标题出显示转发评论(置为 false 则在标题只显示被转发的广播) | 0/1/true/false | false |
| addLinkForPics | 为图片添加可点击的链接 | 0/1/true/false | false |
| showTimestampInDescription | 在正文处显示广播的时间戳 | 0/1/true/false | false |
| showComments | 在正文处显示评论 | 0/1/true/false | false |
| widthOfPics | 广播配图宽(生效取决于阅读器) | 不指定 / 数字 | 不指定 |
| heightOfPics | 广播配图高(生效取决于阅读器) | 不指定 / 数字 | 不指定 |
| sizeOfAuthorAvatar | 作者头像大小 | 数字 | 48 |
指定更多与默认值不同的参数选项可以改善 RSS 的可读性,如
<https://rsshub.app/douban/people/113894409/status/readable=1&authorNameBold=1&showAuthorInTitle=1&showAuthorInDesc=1&showAuthorAvatarInDesc=1&showEmojiForRetweet=1&showRetweetTextInTitle=1&addLinkForPics=1&showTimestampInDescription=1&showComments=1&widthOfPics=100>
的效果为
<img loading="lazy" src="/img/readable-douban.png" alt="豆瓣读书的可读豆瓣广播 RSS" />`
};
function tryFixStatus(status) {
let result = {
isFixSuccess: true,
why: ""
};
const now = /* @__PURE__ */ new Date();
if (!status) {
result = {
isFixSuccess: false,
why: "[ 无内容 ]"
};
status = {};
} else if (status.deleted) result = {
isFixSuccess: false,
why: status.msg ?? "[ 内容已被删除 ]"
};
else if (status.hidden) result = {
isFixSuccess: false,
why: status.msg ?? "[ 内容已被设为不可见 ]"
};
else if (status.text === void 0 || status.text === null || !status.uri) result = {
isFixSuccess: false,
why: status.msg ?? "[ 内容已不可访问 ]"
};
else {
if (!status.author) status.author = {};
if (!status.author.url) status.author.url = "https://www.douban.com/people/1/";
if (!status.author.name) status.author.name = "[作者不可见]";
if (!status.author.avatar) status.author.avatar = "https://img1.doubanio.com/icon/user_normal.jpg";
if (!status.create_time) status.create_time = now.toLocaleString();
if (!status.entities) status.entities = [];
}
if (status.sharing_url) status.sharing_url = status.sharing_url.split("&", 1)[0];
if (!result.isFixSuccess) {
status.sharing_url = "https://www.douban.com?rsshub_failed=" + now.getTime().toString();
if (!status.create_time) status.create_time = now.toLocaleString();
}
return result;
}
function getContentByActivity(ctx, item, params = {}, picsPrefixes = []) {
const routeParams = querystring.parse(ctx.req.param("routeParams"));
params = {
readable: fallback(params.readable, queryToBoolean(routeParams.readable), false),
authorNameBold: fallback(params.authorNameBold, queryToBoolean(routeParams.authorNameBold), false),
showAuthorInTitle: fallback(params.showAuthorInTitle, queryToBoolean(routeParams.showAuthorInTitle), true),
showAuthorInDesc: fallback(params.showAuthorInDesc, queryToBoolean(routeParams.showAuthorInDesc), false),
showAuthorAvatarInDesc: fallback(params.showAuthorAvatarInDesc, queryToBoolean(routeParams.showAuthorAvatarInDesc), false),
showEmojiForRetweet: fallback(params.showEmojiForRetweet, queryToBoolean(routeParams.showEmojiForRetweet), false),
showRetweetTextInTitle: fallback(params.showRetweetTextInTitle, queryToBoolean(routeParams.showRetweetTextInTitle), false),
addLinkForPics: fallback(params.addLinkForPics, queryToBoolean(routeParams.addLinkForPics), false),
showTimestampInDescription: fallback(params.showTimestampInDescription, queryToBoolean(routeParams.showTimestampInDescription), false),
showComments: fallback(params.showComments, queryToBoolean(routeParams.showComments), false),
showColonInDesc: fallback(params.showColonInDesc, null, false),
widthOfPics: fallback(params.widthOfPics, queryToInteger(routeParams.widthOfPics), -1),
heightOfPics: fallback(params.heightOfPics, queryToInteger(routeParams.heightOfPics), -1),
sizeOfAuthorAvatar: fallback(params.sizeOfAuthorAvatar, queryToInteger(routeParams.sizeOfAuthorAvatar), 48)
};
const { readable, authorNameBold, showAuthorInTitle, showAuthorInDesc, showAuthorAvatarInDesc, showEmojiForRetweet, showRetweetTextInTitle, addLinkForPics, showTimestampInDescription, showComments, showColonInDesc, widthOfPics, heightOfPics, sizeOfAuthorAvatar } = params;
function prepareImages(imageUrls) {
if (!imageUrls.length) return "";
const imgTags = [];
for (const url of imageUrls) {
if (!url) {
imgTags.push("[无法显示的图片]");
continue;
}
const attributes = [];
const styleParts = [];
if (widthOfPics >= 0) {
attributes.push(`width="${widthOfPics}"`);
styleParts.push(`width: ${widthOfPics}px;`);
}
if (heightOfPics >= 0) {
attributes.push(`height="${heightOfPics}"`);
styleParts.push(`height: ${heightOfPics}px;`);
}
if (styleParts.length) attributes.push(`style="${styleParts.join(" ")}"`);
if (readable) attributes.push("vspace=\"8\"", "hspace=\"4\"");
const imgTag = `<img ${attributes.join(" ")} src="${url}">`;
const wrappedImage = addLinkForPics ? `<a href="${url}" target="_blank" rel="noopener noreferrer">${imgTag}</a>` : imgTag;
imgTags.push(wrappedImage);
}
if (readable) return imgTags.join("<br>");
return imgTags.join("");
}
const { status, comments } = item;
const { isFixSuccess, why } = tryFixStatus(status);
if (!isFixSuccess) return {
title: why,
description: why
};
let description = "";
let title = "";
let activityInDesc;
let activityInTitle;
const { isFixSuccess: isResharedFixSuccess, why: resharedWhy } = tryFixStatus(status.reshared_status);
if (status.activity === "转发") if (isResharedFixSuccess) {
activityInDesc = "转发 ";
if (readable) activityInDesc += `<a href="${status.reshared_status.author.url}" target="_blank" rel="noopener noreferrer">`;
if (authorNameBold) activityInDesc += "<strong>";
activityInDesc += status.reshared_status.author.name;
if (authorNameBold) activityInDesc += "</strong>";
if (readable) activityInDesc += "</a>";
activityInDesc += " 的广播";
activityInTitle = `转发 ${status.reshared_status.author.name} 的广播`;
} else {
activityInDesc = "转发广播";
activityInTitle = "转发广播";
}
else {
activityInDesc = status.activity;
activityInTitle = status.activity;
}
if (showAuthorInDesc) {
let usernameAndAvatar = "";
if (readable) usernameAndAvatar += `<a href="${status.author.url}" target="_blank" rel="noopener noreferrer">`;
if (showAuthorAvatarInDesc) usernameAndAvatar += `<img width="${sizeOfAuthorAvatar}" height="${sizeOfAuthorAvatar}" src="${status.author.avatar}" ${readable ? "hspace=\"8\" vspace=\"8\" align=\"left\"" : ""} />`;
if (authorNameBold) usernameAndAvatar += "<strong>";
usernameAndAvatar += status.author.name;
if (authorNameBold) usernameAndAvatar += "</strong>";
if (readable) usernameAndAvatar += "</a>";
usernameAndAvatar += " ";
description += usernameAndAvatar + activityInDesc + (showColonInDesc ? ": " : "");
}
if (showAuthorInTitle) title += `${status.author.name} `;
title += `${activityInTitle}: `;
if (showTimestampInDescription) description += `<br><small>${status.create_time}</small><br>`;
let text = status.text;
let lastIndex = 0;
const replacedTextSegements = [];
for (const entity of status.entities) {
replacedTextSegements.push(text.slice(lastIndex, entity.start), `<a href="${entity.uri.replace("douban://douban.com", "https://www.douban.com/doubanapp/dispatch?uri=")}" target="_blank" rel="noopener noreferrer">${entity.title}</a>`);
lastIndex = entity.end;
}
replacedTextSegements.push(text.slice(lastIndex));
text = replacedTextSegements.join("");
description += text;
if (status.card) title += status.card.rating ? `《${status.card.title}》` : `「${status.card.title}」`;
if (status.activity !== "转发" || showRetweetTextInTitle) title += status.text.replace("\n", "");
if (status.images && status.images.length) {
description += readable ? "<br clear=\"both\" /><div style=\"clear: both\"></div>" : "<br>";
let picsPrefix = "";
for (const image of status.images) {
if (!(image.large && image.large.url)) continue;
picsPrefix += `<img width="0" height="0" hidden="true" src="${image.large.url}">`;
}
picsPrefixes.push(picsPrefix);
const imageUrls = Array.from(status.images, (image) => image?.large?.url);
description += prepareImages(imageUrls);
}
if (status.video_info) {
description += readable ? "<br clear=\"both\" /><div style=\"clear: both\"></div>" : "<br>";
const videoCover = status.video_info.cover_url;
const videoSrc = status.video_info.video_url;
if (videoSrc) description = `
${description}
<video
src="${videoSrc}"
${videoCover ? `poster="${videoCover}"` : ""}
>
</video>
`;
}
if (status.parent_status) {
description += showEmojiForRetweet ? " 🔁 " : " Fw: ";
if (showRetweetTextInTitle) title += showEmojiForRetweet ? " 🔁 " : " Fw: ";
const { isFixSuccess: isParentFixSuccess, why: parentWhy } = tryFixStatus(status.parent_status);
if (isParentFixSuccess) {
let usernameAndAvatar = "";
if (readable) usernameAndAvatar += `<a href="${status.parent_status.author.url}">`;
if (authorNameBold) usernameAndAvatar += "<strong>";
usernameAndAvatar += status.parent_status.author.name;
if (authorNameBold) usernameAndAvatar += "</strong>";
if (readable) usernameAndAvatar += "</a>";
usernameAndAvatar += ": ";
description += usernameAndAvatar + status.parent_status.text;
if (showRetweetTextInTitle) title += status.parent_status.author.name + ": " + status.parent_status.text;
} else {
description += parentWhy;
if (showRetweetTextInTitle) title += parentWhy;
}
}
if (status.card) {
if (description) description += readable ? "<br clear=\"both\" /><div style=\"clear: both\"></div><blockquote style=\"background: #80808010;border-top:1px solid #80808030;border-bottom:1px solid #80808030;margin:0;padding:5px 20px;\">" : "<br>";
if (!status.card.images_block && status.card.image) description += `<img src="${status.card.image.large.url}" ${readable ? "vspace=\"0\" hspace=\"12\" align=\"left\" height=\"75\" style=\"height: 75px;\"" : ""} />`;
const isNewReshared = status.activity === "转发小组讨论" || status.card.type === "topic" && status.text !== "" && status.activity === "";
if (!isNewReshared && status.card.type === "topic" && status.text === "" && status.activity === "") status.sharing_url = status.card.url;
const cardContents = [];
if (status.card.title) {
let descTitle = `<strong>${status.card.title}</strong>`;
if (status.card.url) descTitle = `<a href="${status.card.url}" target="_blank" rel="noopener noreferrer">${descTitle}</a>`;
cardContents.push(descTitle);
}
if (status.card.subtitle) {
const prefix = isNewReshared ? `${status.card.owner_name}:` : "";
cardContents.push(prefix + status.card.subtitle);
}
if (status.card.rating) cardContents.push(`评分:${status.card.rating.value}`);
description += cardContents.join("<br>");
if (readable) description += "<br clear=\"both\" /><div style=\"clear: both\"></div></blockquote>";
if (status.card.images_block) {
const imageUrls = Array.from(status.card.images_block.images, (image) => image.image?.large?.url);
description += prepareImages(imageUrls);
}
}
if (status.video_card) {
description += readable ? "<br clear=\"both\" /><div style=\"clear: both\"></div><blockquote style=\"background: #80808010;border-top:1px solid #80808030;border-bottom:1px solid #80808030;margin:0;padding:5px 20px;\">" : "<br>";
const videoCover = status.video_card.video_info && status.video_card.video_info.cover_url;
const videoSrc = status.video_card.video_info && status.video_card.video_info.video_url;
if (!status.video_card.url) status.video_card.url = "https://www.douban.com";
description += `${videoSrc ? `<video src="${videoSrc}" ${videoCover ? `poster="${videoCover}"` : ""}></video>` : ""}<br>${status.video_card.title ? `<a href="${status.video_card.url}">${status.video_card.title}</a>` : ""}`;
if (readable) description += "</blockquote>";
}
if (status.reshared_status) {
description += readable ? "<br clear=\"both\" /><div style=\"clear: both\"></div><blockquote style=\"background: #80808010;border-top:1px solid #80808030;border-bottom:1px solid #80808030;margin:0;padding:5px 20px;\">" : "<br>";
if (showRetweetTextInTitle) title += " | ";
if (isResharedFixSuccess) {
description += getContentByActivity(ctx, {
status: status.reshared_status,
comments: []
}, {
showAuthorInDesc: true,
showAuthorAvatarInDesc: false,
showComments: false,
showColonInDesc: true
}, picsPrefixes).description;
title += status.reshared_status.text;
const reshared_url = status.reshared_status.uri.replace("douban://douban.com", "https://www.douban.com/doubanapp/dispatch?uri=");
if (readable) description += `<br><small>原动态:<a href="${reshared_url}" target="_blank" rel="noopener noreferrer">${reshared_url}</a></small><br clear="both" /><div style="clear: both"></div></blockquote>`;
} else {
description += resharedWhy;
title += resharedWhy;
}
}
if (showComments) {
if (comments.length > 0) description += "<hr>";
for (const comment of comments) description += `<br>${comment.text} - <a href="${comment.author.url}" target="_blank" rel="noopener noreferrer">${comment.author.name}</a>`;
}
if (showAuthorInDesc && showAuthorAvatarInDesc) description = picsPrefixes.join("") + description;
description = description.trim().replaceAll("\n", "<br>");
return {
title,
description
};
}
async function getFullTextItems(items) {
const prefix = "https://m.douban.com/rexxar/api/v2/status/";
await Promise.all(items.map(async (item) => {
let url = prefix + item.status.id;
let cacheResult = await cache_default.get(url);
if (cacheResult) item.status.text = cacheResult;
else {
const { data: { text } } = await got_default(url);
cache_default.set(url, text);
item.status.text = text;
}
if (!item.status.reshared_status) return;
url = prefix + item.status.reshared_status.id;
cacheResult = await cache_default.get(url);
if (cacheResult) item.status.reshared_status.text = cacheResult;
else if (tryFixStatus(item.status.reshared_status).isFixSuccess) try {
const { data: { text } } = await got_default(url);
cache_default.set(url, text);
item.status.reshared_status.text = text;
} catch {
item.status.reshared_status.text += "\n[获取原动态失败]";
}
}));
}
async function handler(ctx) {
const userid = ctx.req.param("userid");
const url = `https://m.douban.com/rexxar/api/v2/status/user_timeline/${userid}`;
const items = await cache_default.tryGet(url, async () => {
return (await got_default(url)).data.items;
}, config.cache.routeExpire, false);
if (items) await getFullTextItems(items);
return {
title: `豆瓣广播 - ${items ? items[0].status.author.name : userid}`,
link: `https://m.douban.com/people/${userid}/statuses`,
item: items && items.filter((item) => !item.deleted).map((item) => {
const r = getContentByActivity(ctx, item);
return {
title: r.title,
link: item.status.sharing_url.replace(/\?_i=(.*)/, ""),
pubDate: new Date(Date.parse(item.status.create_time + " GMT+0800")).toUTCString(),
description: r.description
};
})
};
}
//#endregion
export { route };