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>
This commit is contained in:
parent
271efc81ba
commit
44f1ea3718
|
|
@ -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: '文学城',
|
||||
|
|
|
|||
|
|
@ -187,7 +187,22 @@ These feed do not include boosts (a.k.a. reblogs). RSSHub provides a feed for us
|
|||
|
||||
### User Profile
|
||||
|
||||
<RouteEn author="hoilc" example="/picuki/profile/stefaniejoosten" path="/picuki/profile/:id/:displayVideo?" :paramsDesc="['Instagram id','Default to disable the embedded video, set to any value to enable embedding']" />
|
||||
<RouteEn author="hoilc Rongronggg9" example="/picuki/profile/stefaniejoosten" path="/picuki/profile/:id/:functionalFlag?" :paramsDesc="['Instagram id','functional flag, see the table below']" radar="1" rssbud="1">
|
||||
|
||||
| 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.
|
||||
|
||||
:::
|
||||
|
||||
</RouteEn>
|
||||
|
||||
## pixiv
|
||||
|
||||
|
|
|
|||
|
|
@ -519,7 +519,22 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
|
|||
|
||||
### 用户
|
||||
|
||||
<Route author="hoilc" example="/picuki/profile/stefaniejoosten" path="/picuki/profile/:id/:displayVideo?" :paramsDesc="['Instagram 用户 id','是否显示视频,任意值为是,留空为否']" radar="1" rssbud="1"/>
|
||||
<Route author="hoilc Rongronggg9" example="/picuki/profile/stefaniejoosten" path="/picuki/profile/:id/:functionalFlag?" :paramsDesc="['Instagram 用户 id','功能标记,见下表']" radar="1" rssbud="1">
|
||||
|
||||
| functionalFlag | 嵌入视频 | 获取 Instagram Stories |
|
||||
|----------------|--------------|----------------------|
|
||||
| 0 | 关,只用图片显示视频封面 | 关 |
|
||||
| 1 (默认) | 开 | 关 |
|
||||
| 10 | 开 | 开 |
|
||||
|
||||
::: warning 注意
|
||||
|
||||
Instagram Stories 没有可靠的 guid,你的 RSS 阅读器可能将同一条 Story 显示多于一次。
|
||||
尽管如此,每个 Story 都会在 24 小时后过期,所以问题也许没那么严重。
|
||||
|
||||
:::
|
||||
|
||||
</Route>
|
||||
|
||||
## pixiv
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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 ? `<img src='${poster}' alt='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 ? `<p>${post_description}</p>` : '',
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
'/profile/:id/:displayVideo?': ['hoilc', 'Rongronggg9'],
|
||||
};
|
||||
|
|
@ -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 ? `<img src='${poster}' alt='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 = `<video controls src="${href}" poster="${poster || storiesBackgroundUrlSrc || ''}">`;
|
||||
} else if (type === 'image') {
|
||||
description = `<img src="${href || poster || storiesBackgroundUrlSrc || ''}" alt="Instagram Story">`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Instagram Story',
|
||||
author: `@${id}`,
|
||||
description,
|
||||
link: href,
|
||||
pubDate,
|
||||
};
|
||||
});
|
||||
if (storyItems.length) {
|
||||
items = items.concat(storyItems);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (includeStories) {
|
||||
let profileQueryId;
|
||||
$('script:not([src])').each((_, script) => {
|
||||
const queryMatch = $(script)
|
||||
.html()
|
||||
.match(/let query ?= ?['"](\d+)['"]/);
|
||||
if (queryMatch) {
|
||||
profileQueryId = queryMatch[1];
|
||||
}
|
||||
});
|
||||
if (profileQueryId) {
|
||||
await getStories(profileQueryId);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.state.data = {
|
||||
title: `${profileName} (@${id}) - Picuki`,
|
||||
link: profileUrl,
|
||||
image: profileImg,
|
||||
description: profileDescription,
|
||||
item: items,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = function (router) {
|
||||
router.get('/profile/:id/:functionalFlag?', require('./profile'));
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{{ if media }}{{@ media.replaceAll('\n', '') }}{{ /if }}
|
||||
{{ if desc }}<p>{{@ desc.replaceAll('\n', '<br>') }}</p>{{ /if }}
|
||||
{{ if locationLink.length && locationLink.attr('href') }}
|
||||
<p>📍 <small><a href="{{ locationLink.attr('href') }}">{{ locationLink.text() }}</a></small></p>
|
||||
{{ else if locationLink.length }}
|
||||
<p>📍 <small>{{ locationLink.text() }}</small></p>
|
||||
{{ /if }}
|
||||
Loading…
Reference in New Issue