diff --git a/docs/.vuepress/public/readable-douban.png b/docs/.vuepress/public/readable-douban.png new file mode 100644 index 000000000..727e3b287 Binary files /dev/null and b/docs/.vuepress/public/readable-douban.png differ diff --git a/docs/social-media.md b/docs/social-media.md index 06b836e82..71d1a1bcf 100644 --- a/docs/social-media.md +++ b/docs/social-media.md @@ -694,7 +694,7 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性 ### 用户广播 - + ::: 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 + +的效果为 + +豆瓣读书的可读豆瓣广播 RSS + ### 日记最新回应 diff --git a/lib/router.js b/lib/router.js index b360f60c9..468d2f62a 100644 --- a/lib/router.js +++ b/lib/router.js @@ -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')); diff --git a/lib/routes/douban/people/status.js b/lib/routes/douban/people/status.js index 4f6c1173f..03732f267 100644 --- a/lib/routes/douban/people/status.js +++ b/lib/routes/douban/people/status.js @@ -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 += `

`; - }); - break; - case '转发': - description = `${status.text}
`; - 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 += `

`; - }); - } - break; - default: - description = `${status.text ? `${status.text}

` : ''}`; - 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}
${status.card.subtitle}${image ? `
` : ''}
《${status.card.title}》`; - title = `${status.author.name}${status.activity}:《${status.card.title}》`; - } else { - description += `${status.card.subtitle}${image ? `
` : ''}
${status.card.url ? `${status.card.title}` : ''}`; - 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 += ``; } - 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 ? `
` : ''}

${ - status.video_card.title ? `
${status.video_card.title}` : '' - }`; - title = `${status.author.name}${status.activity}:《${status.video_card.title}》`; + if (authorNameBold) { + activityInDesc += ``; } - break; + activityInDesc += status.reshared_status.author.name; + if (authorNameBold) { + activityInDesc += ``; + } + if (readable) { + activityInDesc += ``; + } + activityInDesc += ` 的广播`; + activityInTitle = `转发 ${status.reshared_status.author.name} 的广播`; + } } + + if (showAuthorInDesc) { + let usernameAndAvatar = ''; + if (readable) { + usernameAndAvatar += ``; + } + if (showAuthorAvatarInDesc) { + usernameAndAvatar += ``; + } + if (authorNameBold) { + usernameAndAvatar += ``; + } + usernameAndAvatar += status.author.name; + if (authorNameBold) { + usernameAndAvatar += ``; + } + if (readable) { + usernameAndAvatar += ``; + } + usernameAndAvatar += ` `; + description += usernameAndAvatar + activityInDesc + (showColonInDesc ? ': ' : ''); + } + + if (showAuthorInTitle) { + title += `${status.author.name} `; + } + title += `${activityInTitle}: `; + + if (showTimestampInDescription) { + description += `
${status.create_time}
`; + } + + let text = status.text; + let lastIndex = 0; + const replacedTextSegements = []; + for (const entity of status.entities) { + replacedTextSegements.push(text.slice(lastIndex, entity.start)); + replacedTextSegements.push(`${entity.title}`); + 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 += `
`; + } else { + description += `
`; + } + + // 一些RSS Reader会识别所有标签作为内含图片显示,我们不想要头像也作为内含图片之一 + // 让所有配图在description的最前面再次出现一次,但宽高设为0 + let picsPrefix = ''; + status.images.forEach(function (image) { + picsPrefix += ``; + }); + picsPrefixes.push(picsPrefix); + + status.images.forEach((image) => { + if (addLinkForPics) { + description += ''; + } + if (!readable) { + description += '
'; + } + let style = ''; + description += '= 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 += '
'; + } + }); + } + + if (status.video_info) { + if (readable) { + description += `
`; + } else { + description += `
`; + } + const videoCover = status.video_info.cover_url; + const videoSrc = status.video_info.video_url; + description += `${videoSrc ? `` : ''}`; + } + + if (status.parent_status) { + description += showEmojiForRetweet ? ' 🔁 ' : ' Fw: '; + if (showRetweetTextInTitle) { + title += showEmojiForRetweet ? ' 🔁 ' : ' Fw: '; + } + + let usernameAndAvatar = ''; + + if (readable) { + usernameAndAvatar += ``; + } + if (authorNameBold) { + usernameAndAvatar += ``; + } + usernameAndAvatar += status.parent_status.author.name; + if (authorNameBold) { + usernameAndAvatar += ``; + } + if (readable) { + usernameAndAvatar += ``; + } + usernameAndAvatar += `: `; + 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 += `
`; + } else { + description += `
`; + } + if (image) { + description += ``; + } + description += `${status.card.title}
${status.card.subtitle}`; + if (status.card.rating) { + description += `
评分:${status.card.rating}`; + } + description += `
`; + if (readable) { + description += `
`; + } + } + + // video_card + if (status.video_card) { + if (readable) { + description += `
`; + } else { + description += `
`; + } + 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 ? `` : ''}
${status.video_card.title ? `${status.video_card.title}` : ''}`; + if (readable) { + description += `
`; + } + } + + // reshared_status + if (status.reshared_status) { + if (readable) { + description += `
`; + } else { + description += `
`; + } + + 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 += `
原动态:${reshared_url}
`; + } + } + } + + // comments + if (showComments) { + if (comments.length > 0) { + description += '
'; + } + for (const comment of comments) { + description += `
${comment.text} - ${comment.author.name}`; + } + } + + if (showAuthorInDesc && showAuthorAvatarInDesc) { + description = picsPrefixes.join('') + description; + } + description = description.trim().replace(/\n/g, '
'); 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,