diff --git a/assets/radar-rules.js b/assets/radar-rules.js index fd17d9df7..c53c451a9 100644 --- a/assets/radar-rules.js +++ b/assets/radar-rules.js @@ -924,7 +924,6 @@ }, ], }, - 'picuki.com': { _name: 'Picuki', www: [{ title: '用户', docs: 'https://docs.rsshub.app/social-media.html#picuki-yong-hu', source: '/profile/:id', target: '/picuki/profile/:id' }] }, 'jjmhw.cc': { _name: '漫小肆', www: [{ title: '漫画更新', docs: 'https://docs.rsshub.app/anime.html#man-xiao-si', source: '/book/:id', target: '/manxiaosi/book/:id' }] }, 'wenxuecity.com': { _name: '文学城', diff --git a/docs/en/social-media.md b/docs/en/social-media.md index cfdd423ca..d230f4a20 100644 --- a/docs/en/social-media.md +++ b/docs/en/social-media.md @@ -187,7 +187,22 @@ These feed do not include boosts (a.k.a. reblogs). RSSHub provides a feed for us ### User Profile - + + +| functionalFlag | Video embedding | Fetching Instagram Stories | +|----------------|-----------------------------------------|----------------------------| +| 0 | off, only show video poster as an image | off | +| 1 (default) | on | off | +| 10 | on | on | + +::: warning + +Instagram Stories do not have a reliable guid. It is possible that your RSS reader show the same story more than once. +Though, every Story expires after 24 hours, so it may be not so serious. + +::: + + ## pixiv diff --git a/docs/social-media.md b/docs/social-media.md index 22e7e9927..0cc20bb2b 100644 --- a/docs/social-media.md +++ b/docs/social-media.md @@ -519,7 +519,22 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性 ### 用户 - + + +| functionalFlag | 嵌入视频 | 获取 Instagram Stories | +|----------------|--------------|----------------------| +| 0 | 关,只用图片显示视频封面 | 关 | +| 1 (默认) | 开 | 关 | +| 10 | 开 | 开 | + +::: warning 注意 + +Instagram Stories 没有可靠的 guid,你的 RSS 阅读器可能将同一条 Story 显示多于一次。 +尽管如此,每个 Story 都会在 24 小时后过期,所以问题也许没那么严重。 + +::: + + ## pixiv diff --git a/lib/radar-rules.js b/lib/radar-rules.js index f8d53284b..59e756f7b 100644 --- a/lib/radar-rules.js +++ b/lib/radar-rules.js @@ -2018,17 +2018,6 @@ module.exports = { }, ], }, - 'picuki.com': { - _name: 'Picuki', - www: [ - { - title: '用户', - docs: 'https://docs.rsshub.app/social-media.html#picuki-yong-hu', - source: '/profile/:id', - target: '/picuki/profile/:id', - }, - ], - }, 'jjmhw.cc': { _name: '漫小肆', www: [ diff --git a/lib/router.js b/lib/router.js index 86b281f7c..9c38be760 100644 --- a/lib/router.js +++ b/lib/router.js @@ -3111,9 +3111,6 @@ router.get('/behance/:user/:type?', lazyloadRouteHandler('./routes/behance/index // 北京物资学院 router.get('/bwu/news', lazyloadRouteHandler('./routes/universities/bwu/news')); -// Picuki -router.get('/picuki/profile/:id/:displayVideo?', lazyloadRouteHandler('./routes/picuki/profile')); - // 新榜 router.get('/newrank/wechat/:wxid', lazyloadRouteHandler('./routes/newrank/wechat')); router.get('/newrank/douyin/:dyid', lazyloadRouteHandler('./routes/newrank/douyin')); diff --git a/lib/routes/picuki/profile.js b/lib/routes/picuki/profile.js deleted file mode 100644 index 0e91e10d8..000000000 --- a/lib/routes/picuki/profile.js +++ /dev/null @@ -1,90 +0,0 @@ -const got = require('@/utils/got'); -const cheerio = require('cheerio'); -const chrono = require('chrono-node'); - -module.exports = async (ctx) => { - const id = ctx.params.id; - const displayVideo = !!ctx.params.displayVideo; - - const url = `https://www.picuki.com/profile/${id}`; - - const response = await got.get(url); - const $ = cheerio.load(response.data); - - const profile_name = $('.profile-name-bottom').text(); - const profile_img = $('.profile-avatar > img').attr('src'); - const profile_description = $('.profile-description').text(); - - const list = $('ul.box-photos [data-s="media"]').get(); - - async function getMedia(url) { - const response = await got.get(url); - const $ = cheerio.load(response.data); - // Instagram 允许最多 10 条图片/视频任意混合于一条 post,picuki 在所有情况下都会将它(们)置于 .single-photo 内 - const media_items = $('.single-photo img,video').get(); - let html = ''; - media_items.forEach((item) => { - item = $(item); - html += item.toString(); // 可能是视频,也可能是图片,格式都比较友好,这里直接添加 - }); - return html; - } - - function deVideo(media) { - const $ = cheerio.load(media); - const media_list = $('img,video').get(); - let media_deVideo = ''; - media_list.forEach((medium) => { - medium = $(medium); - const poster = medium.attr('poster'); - // 如果有 poster 属性,表明它是视频,将它替换为它的 poster;如果不是,就原样返回 - media_deVideo += poster ? `video poster` : medium.toString(); - }); - return media_deVideo; - } - - const items = await Promise.all( - list.map(async (post) => { - post = $(post); - - const post_link = post.find('.photo > a').attr('href'); - const cache = await ctx.cache.get(post_link); - - let cache_d; - if (cache) { - cache_d = JSON.parse(cache); - } else { - const post_description = post.find('.photo-description').text().trim(); - const post_time = post.find('.time'); - const media_displayVideo = await getMedia(post_link); - const media = deVideo(media_displayVideo); - - cache_d = { - title: post_description || 'Untitled', - author: `@${id}`, - media_prefix: media, - media_displayVideo_prefix: media_displayVideo, - description_body: post_description.length > 100 ? `

${post_description}

` : '', - link: post_link, - pubDate: post_time ? chrono.parseDate(post_time.text()) : new Date(), - }; - - ctx.cache.set(post_link, JSON.stringify(cache_d)); - } - - cache_d.description = (displayVideo ? cache_d.media_displayVideo_prefix : cache_d.media_prefix) + cache_d.description_body; - delete cache_d.media_prefix; - delete cache_d.media_displayVideo_prefix; - delete cache_d.description_body; - return cache_d; - }) - ); - - ctx.state.data = { - title: `${profile_name} (@${id}) - Picuki`, - link: url, - image: profile_img, - description: profile_description, - item: items, - }; -}; diff --git a/lib/v2/picuki/maintainer.js b/lib/v2/picuki/maintainer.js new file mode 100644 index 000000000..592b1816b --- /dev/null +++ b/lib/v2/picuki/maintainer.js @@ -0,0 +1,3 @@ +module.exports = { + '/profile/:id/:displayVideo?': ['hoilc', 'Rongronggg9'], +}; diff --git a/lib/v2/picuki/profile.js b/lib/v2/picuki/profile.js new file mode 100644 index 000000000..4eac2f691 --- /dev/null +++ b/lib/v2/picuki/profile.js @@ -0,0 +1,167 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); +const chrono = require('chrono-node'); +const { art } = require('@/utils/render'); +const path = require('path'); + +module.exports = async (ctx) => { + const id = ctx.params.id; + const displayVideo = ctx.params.functionalFlag !== '0'; + const includeStories = ctx.params.functionalFlag === '10'; + + const profileUrl = `https://www.picuki.com/profile/${id}`; + + const response = await got.get(profileUrl); + const $ = cheerio.load(response.data); + + const profileName = $('.profile-name-bottom').text(); + const profileImg = $('.profile-avatar > img').attr('src'); + const profileDescription = $('.profile-description').text(); + + const list = $('ul.box-photos [data-s="media"]').get(); + + async function getMedia(url) { + const getPost = async () => { + const response = await got.get(url, { + headers: { + Referer: profileUrl, + }, + }); + return response.data; + }; + let data = await ctx.cache.tryGet(url, getPost); + if (Object.prototype.toString.call(data) === '[object Object]') { + // oops, it's a json, maybe it's an old cache from the old version of this route! + data = await getPost(); + // re-cache it + await ctx.cache.set(url, data); + } + const $ = cheerio.load(data); + // Instagram 允许最多 10 条图片/视频任意混合于一条 post,picuki 在所有情况下都会将它(们)置于 .single-photo 内 + let html = ''; + $('.single-photo img,video').each((_, item) => { + html += $(item).toString(); // 可能是视频,也可能是图片,格式都比较友好,这里直接添加 + }); + return html; + } + + function deVideo(media) { + const $ = cheerio.load(media); + let media_deVideo = ''; + $('img,video').each((_, medium) => { + const tag = medium.name; + medium = $(medium); + const poster = medium.attr('poster'); + // 如果有 poster 属性,表明它是视频,将它替换为它的 poster;如果不是,就原样返回 + media_deVideo += poster ? `video poster` : tag === 'img' ? medium.toString() : ''; + }); + return media_deVideo; + } + + let items = await Promise.all( + list.map(async (post) => { + post = $(post); + + const postLink = post.find('.photo > a').attr('href'); + const postTime = post.find('.time'); + const pubDate = postTime ? chrono.parseDate(postTime.text()) : new Date(); + const media_displayVideo = await getMedia(postLink); + const postText = post + .find('.photo-description') + .text() + .trim() + .replaceAll(/[^\S\n]+/g, ' ') + .replaceAll(/((?<=\n|^)[^\S\n])|([^\S\n](?=\n|$))/g, ''); + const title = postText.replaceAll('\n', ' ') || 'Untitled'; + const description = art(path.join(__dirname, 'templates/post.art'), { + media: displayVideo ? media_displayVideo : deVideo(media_displayVideo), + desc: postText, + locationLink: post.find('.photo-location .icon-globe-alt a'), + }); + + return { + title, + author: `@${id}`, + description, + link: postLink, + pubDate, + }; + }) + ); + + const getStories = async (queryId) => { + const apiUrl = `https://www.picuki.com/app/controllers/ajax.php`; + const key = `${apiUrl}?query=${queryId}&type=story`; + const data = await ctx.cache.tryGet(key, async () => { + const response = await got.post(apiUrl, { + form: { + query: queryId, + type: 'story', + }, + headers: { + Referer: profileUrl, + Origin: 'https://www.picuki.com', + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + return response.data; + }); + if (data && data.stories_container) { + const $ = cheerio.load(data.stories_container); + const storyItems = $('.item') + .get() + .map((item) => { + const $item = $(item); + const postTime = $item.find('.stories_count'); + const pubDate = postTime.length ? chrono.parseDate(postTime.text()) : new Date(); + const postBox = $item.find('.launchLightbox'); + const poster = postBox.attr('data-video-poster'); + const href = postBox.attr('href'); + const type = postBox.attr('data-post-type'); // video / image + const storiesBackground = $item.find('.stories_background'); + const storiesBackgroundUrl = storiesBackground && storiesBackground.css('background-image').match(/url\('?(.*)'?\)/); + const storiesBackgroundUrlSrc = storiesBackgroundUrl && storiesBackgroundUrl[1]; + let description; + if (type === 'video') { + description = `