feat: more readable douban user broadcast (#6053)

This commit is contained in:
Shun Zi 2020-10-30 17:09:58 +08:00 committed by GitHub
parent 3442ca9196
commit ab843e4e0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 328 additions and 48 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

View File

@ -694,7 +694,7 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
### 用户广播
<Route author="alfredcai" example="/douban/people/62759792/status" path="douban/people/:userid/status" :paramsDesc="['整数型用户 id']" radar="1">
<Route author="alfredcai" example="/douban/people/62759792/status" path="douban/people/:userid/status/:routeParams" :paramsDesc="['整数型用户 id', '额外参数;见下']" radar="1">
::: tip 提示
@ -704,6 +704,32 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
:::
对于豆瓣用户广播内容,在 `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 src="/readable-douban.png" alt="豆瓣读书的可读豆瓣广播 RSS">
</Route>
### 日记最新回应

View File

@ -183,7 +183,7 @@ router.get('/douban/bookstore', require('./routes/douban/bookstore'));
router.get('/douban/book/rank/:type?', require('./routes/douban/book/rank'));
router.get('/douban/doulist/:id', require('./routes/douban/doulist'));
router.get('/douban/explore/column/:id', require('./routes/douban/explore_column'));
router.get('/douban/people/:userid/status', require('./routes/douban/people/status.js'));
router.get('/douban/people/:userid/status/:routeParams?', require('./routes/douban/people/status.js'));
router.get('/douban/replies/:uid', require('./routes/douban/replies'));
router.get('/douban/topic/:id/:sort?', require('./routes/douban/topic.js'));
router.get('/douban/channel/:id/:nav?', require('./routes/douban/channel/topic.js'));

View File

@ -1,56 +1,310 @@
const querystring = require('querystring');
const got = require('@/utils/got');
const { fallback, queryToBoolean, queryToInteger } = require('@/utils/readable-social');
function getContentByActivity(status) {
function getContentByActivity(ctx, item, params = {}, picsPrefixes = []) {
const routeParams = querystring.parse(ctx.params.routeParams);
const mergedParams = {
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),
};
params = mergedParams;
const {
readable,
authorNameBold,
showAuthorInTitle,
showAuthorInDesc,
showAuthorAvatarInDesc,
showEmojiForRetweet,
showRetweetTextInTitle,
addLinkForPics,
showTimestampInDescription,
showComments,
showColonInDesc,
widthOfPics,
heightOfPics,
sizeOfAuthorAvatar,
} = params;
const { status, comments } = item;
let description = '';
let title = '';
switch (status.activity) {
case '说':
title = `${status.author.name} ${status.activity}:${status.text}`;
description = status.text;
status.images.forEach((image) => {
description += `<p><img src="${image.large.url}"/></p>`;
});
break;
case '转发':
description = `${status.text}<br>`;
if (status.reshared_status.deleted) {
title = `${status.author.name} ${status.activity} 广播:原动态已被发布者删除`;
description += `原动态已被发布者删除`;
} else {
description += getContentByActivity(status.reshared_status).title;
title = `${status.author.name} ${status.activity} ${status.reshared_status.author.name} 的广播:${status.reshared_status.text}`;
status.reshared_status.images.forEach((image) => {
description += `<p><img src="${image.large.url}"/></p>`;
});
}
break;
default:
description = `${status.text ? `${status.text}<br><br>` : ''}`;
title = `${status.author.name}${status.activity}: ${description}`;
if (status.card) {
let image;
if (status.card.image && (status.card.image.large || status.card.image.normal)) {
image = (status.card.image.large || status.card.image.normal).url;
}
if (status.card.rating) {
description += `豆瓣评分:${status.card.rating}<br>${status.card.subtitle}${image ? `<br><img src="${image}">` : ''}<br><a href="${status.card.url}">《${status.card.title}》</a>`;
title = `${status.author.name}${status.activity}:《${status.card.title}`;
} else {
description += `${status.card.subtitle}${image ? `<br><img src="${image}">` : ''}<br>${status.card.url ? `<a href="${status.card.url}">${status.card.title}</a>` : ''}`;
title = `${status.author.name} ${status.activity}:「${status.card.title}${status.text}`;
}
let activityInDesc;
let activityInTitle;
if (status.activity !== '转发') {
activityInDesc = status.activity;
activityInTitle = status.activity;
} else {
if (status.reshared_status.deleted) {
activityInDesc = `转发广播`;
activityInTitle = `转发广播`;
} else {
activityInDesc = '转发 ';
if (readable) {
activityInDesc += `<a href="${status.reshared_status.author.url}" target="_blank" rel="noopener noreferrer">`;
}
if (status.video_card) {
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;
description += `${videoSrc ? `<br><video src="${videoSrc}" ${videoCover ? `poster="${videoCover}"` : ''}></video>` : ''}<br><br>${
status.video_card.title ? `<a href="${status.video_card.url}">${status.video_card.title}</a>` : ''
}`;
title = `${status.author.name}${status.activity}:《${status.video_card.title}`;
if (authorNameBold) {
activityInDesc += `<strong>`;
}
break;
activityInDesc += status.reshared_status.author.name;
if (authorNameBold) {
activityInDesc += `</strong>`;
}
if (readable) {
activityInDesc += `</a>`;
}
activityInDesc += ` 的广播`;
activityInTitle = `转发 ${status.reshared_status.author.name} 的广播`;
}
}
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 += `&ensp;`;
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));
replacedTextSegements.push(`<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('');
// text // images // video_info // parent status
description += text;
if (status.card) {
if (status.card.rating) {
title += `${status.card.title}`;
} else {
title += `${status.card.title}`;
}
}
if (status.activity !== '转发' || showRetweetTextInTitle) {
title += status.text.replace('\n', '');
}
if (status.images.length) {
if (readable) {
description += `<br clear="both" /><div style="clear: both"></div>`;
} else {
description += `<br>`;
}
// 一些RSS Reader会识别所有<img>标签作为内含图片显示,我们不想要头像也作为内含图片之一
// 让所有配图在description的最前面再次出现一次但宽高设为0
let picsPrefix = '';
status.images.forEach(function (image) {
picsPrefix += `<img width="0" height="0" hidden="true" src="${image.large.url}">`;
});
picsPrefixes.push(picsPrefix);
status.images.forEach((image) => {
if (addLinkForPics) {
description += '<a href="' + image.large.url + '" target="_blank" rel="noopener noreferrer">';
}
if (!readable) {
description += '<br>';
}
let style = '';
description += '<img ';
if (widthOfPics >= 0) {
description += ` width="${widthOfPics}"`;
style += `width: ${widthOfPics}px;`;
}
if (heightOfPics >= 0) {
description += `height="${heightOfPics}" `;
style += `height: ${heightOfPics}px;`;
}
description += ` style="${style}" ` + (readable ? 'vspace="8" hspace="4" ' : '') + ' src="' + image.large.url + '">';
if (addLinkForPics) {
description += '</a>';
}
});
}
if (status.video_info) {
if (readable) {
description += `<br clear="both" /><div style="clear: both"></div>`;
} else {
description += `<br>`;
}
const videoCover = status.video_info.cover_url;
const videoSrc = status.video_info.video_url;
description += `${videoSrc ? `<video src="${videoSrc}" ${videoCover ? `poster="${videoCover}"` : ''}></video>` : ''}`;
}
if (status.parent_status) {
description += showEmojiForRetweet ? ' 🔁 ' : ' Fw: ';
if (showRetweetTextInTitle) {
title += showEmojiForRetweet ? ' 🔁 ' : ' Fw: ';
}
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 += `:&ensp;`;
description += usernameAndAvatar + status.parent_status.text;
if (showRetweetTextInTitle) {
title += status.parent_status.author.name + ': ' + status.parent_status.text;
}
}
// card
if (status.card) {
let image;
if (status.card.image && (status.card.image.large || status.card.image.normal)) {
image = status.card.image.large || status.card.image.normal;
}
if (readable) {
description += `<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;">`;
} else {
description += `<br>`;
}
if (image) {
description += `<img src="${image.url}" ${readable ? 'vspace="0" hspace="12" align="left" height="75" style="height: 75px;"' : ''} />`;
}
description += `<a href="${status.card.url}" target="_blank" rel="noopener noreferrer"><strong>${status.card.title}</strong><br><small>${status.card.subtitle}</small>`;
if (status.card.rating) {
description += `<br><small>评分:${status.card.rating}</small>`;
}
description += `</a>`;
if (readable) {
description += `<br clear="both" /><div style="clear: both"></div></blockquote>`;
}
}
// video_card
if (status.video_card) {
if (readable) {
description += `<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;">`;
} else {
description += `<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;
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>`;
}
}
// reshared_status
if (status.reshared_status) {
if (readable) {
description += `<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;">`;
} else {
description += `<br>`;
}
if (showRetweetTextInTitle) {
title += ' | ';
}
if (status.reshared_status.deleted) {
description += `原动态不可访问`;
title += `原动态不可访问`;
} else {
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>`;
}
}
}
// comments
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().replace(/\n/g, '<br>');
return { title, description };
}
@ -78,7 +332,7 @@ module.exports = async (ctx) => {
items
.filter((item) => !item.deleted)
.map((item) => {
const r = getContentByActivity(item.status);
const r = getContentByActivity(ctx, item);
return {
title: r.title,
link: item.status.sharing_url,