From 44f1ea37180c2ffbc1897a85ba8d72028c2c83ea Mon Sep 17 00:00:00 2001
From: Rongrong <15956627+Rongronggg9@users.noreply.github.com>
Date: Wed, 2 Mar 2022 22:26:21 +0800
Subject: [PATCH] feat(route)(picuki/profile): migrate to v2, fetch stories
(#9216)
* refactor(route)(picuki/profile): migrate to v2
other changes:
fix linebreak
fix header
add location
`displayVideo` param defaults to enable
Signed-off-by: Rongrong <15956627+Rongronggg9@users.noreply.github.com>
* refactor(route)(picuki/profile): use template
Signed-off-by: Rongrong <15956627+Rongronggg9@users.noreply.github.com>
* feat(route)(picuki/profile): fetch stories
Signed-off-by: Rongrong <15956627+Rongronggg9@users.noreply.github.com>
---
assets/radar-rules.js | 1 -
docs/en/social-media.md | 17 +++-
docs/social-media.md | 17 +++-
lib/radar-rules.js | 11 --
lib/router.js | 3 -
lib/routes/picuki/profile.js | 90 -----------------
lib/v2/picuki/maintainer.js | 3 +
lib/v2/picuki/profile.js | 167 +++++++++++++++++++++++++++++++
lib/v2/picuki/radar.js | 13 +++
lib/v2/picuki/router.js | 3 +
lib/v2/picuki/templates/post.art | 7 ++
11 files changed, 225 insertions(+), 107 deletions(-)
delete mode 100644 lib/routes/picuki/profile.js
create mode 100644 lib/v2/picuki/maintainer.js
create mode 100644 lib/v2/picuki/profile.js
create mode 100644 lib/v2/picuki/radar.js
create mode 100644 lib/v2/picuki/router.js
create mode 100644 lib/v2/picuki/templates/post.art
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 ? `
` : 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 ? `
` : 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 = `