diff --git a/docs/en/bbs.md b/docs/en/bbs.md
index f2deab5d1..fff889c13 100644
--- a/docs/en/bbs.md
+++ b/docs/en/bbs.md
@@ -62,6 +62,20 @@ pageClass: routes
+## Pikabu
+
+### Community
+
+
+
+### Tag
+
+
+
+### User
+
+
+
## SCBOY forum
### Thread
@@ -78,4 +92,4 @@ When accessing Joeyray's Bar, `SCBOY_BBS_TOKEN` needs to be filled in `environme
### forum
-
+
diff --git a/lib/v2/pikabu/community.js b/lib/v2/pikabu/community.js
new file mode 100644
index 000000000..45c71feae
--- /dev/null
+++ b/lib/v2/pikabu/community.js
@@ -0,0 +1,37 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const { parseDate } = require('@/utils/parse-date');
+const { baseUrl, fixImage, fixVideo } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { type, name, sort = 'new' } = ctx.params;
+ const sortString = sort === 'default' || type === 'tag' ? '' : `/${sort}`;
+ const { data: response } = await got(`${baseUrl}/ajax/${type}/${name}${sortString}`);
+
+ const items = response.data.stories.map((story) => {
+ const $ = cheerio.load(story.html, null, false);
+ const data = JSON.parse($('script[type="application/ld+json"]').text());
+
+ const content = $('.story__main');
+
+ fixImage(content);
+ content.find('.player').each((_, elem) => {
+ elem = $(elem);
+ fixVideo(elem);
+ });
+ return {
+ title: data.name,
+ description: content.find('.story__content-inner').html(),
+ pubDate: parseDate(data.dateCreated),
+ author: data.author.name,
+ link: data.url,
+ };
+ });
+
+ ctx.state.data = {
+ title: response.data.title,
+ link: `${baseUrl}/${type}/${name}`,
+ language: 'ru-RU',
+ item: items,
+ };
+};
diff --git a/lib/v2/pikabu/maintainer.js b/lib/v2/pikabu/maintainer.js
new file mode 100644
index 000000000..036479c63
--- /dev/null
+++ b/lib/v2/pikabu/maintainer.js
@@ -0,0 +1,5 @@
+module.exports = {
+ '/community/:name': ['TonyRL'],
+ '/tag/:name': ['TonyRL'],
+ '/user/:name': ['TonyRL'],
+};
diff --git a/lib/v2/pikabu/radar.js b/lib/v2/pikabu/radar.js
new file mode 100644
index 000000000..4a72d4726
--- /dev/null
+++ b/lib/v2/pikabu/radar.js
@@ -0,0 +1,25 @@
+module.exports = {
+ 'pikabu.ru': {
+ _name: 'Pikabu',
+ '.': [
+ {
+ title: 'Community',
+ docs: 'https://docs.rsshub.app/en/bbs.html#pikabu',
+ source: ['/community/:name'],
+ target: '/pikabu/community/:name',
+ },
+ {
+ title: 'Tag',
+ docs: 'https://docs.rsshub.app/en/bbs.html#pikabu',
+ source: ['/tag/:name'],
+ target: '/pikabu/tag/:name',
+ },
+ {
+ title: 'User',
+ docs: 'https://docs.rsshub.app/en/bbs.html#pikabu',
+ source: ['/:name'],
+ target: '/pikabu/user/:name',
+ },
+ ],
+ },
+};
diff --git a/lib/v2/pikabu/router.js b/lib/v2/pikabu/router.js
new file mode 100644
index 000000000..0483a08e8
--- /dev/null
+++ b/lib/v2/pikabu/router.js
@@ -0,0 +1,4 @@
+module.exports = (router) => {
+ router.get('/user/:name', require('./user'));
+ router.get('/:type/:name', require('./community'));
+};
diff --git a/lib/v2/pikabu/templates/video.art b/lib/v2/pikabu/templates/video.art
new file mode 100644
index 000000000..f76b0437e
--- /dev/null
+++ b/lib/v2/pikabu/templates/video.art
@@ -0,0 +1,8 @@
+{{ if videoId }}
+
+{{ else if webm || mp4 }}
+
+{{ /if }}
diff --git a/lib/v2/pikabu/user.js b/lib/v2/pikabu/user.js
new file mode 100644
index 000000000..25fbbd6fc
--- /dev/null
+++ b/lib/v2/pikabu/user.js
@@ -0,0 +1,46 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const iconv = require('iconv-lite');
+const { parseDate } = require('@/utils/parse-date');
+const { baseUrl, fixImage, fixVideo } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { name } = ctx.params;
+ const link = `${baseUrl}/${name}`;
+ const response = await got(link, {
+ responseType: 'buffer',
+ });
+
+ const charset = response.headers['content-type'].match(/charset=([\w-]+)/)[1]; // windows-1251
+ const $ = cheerio.load(iconv.decode(response.data, charset));
+
+ const items = $('.story__main')
+ .not('.story__placeholder')
+ .toArray()
+ .map((story) => {
+ story = $(story);
+
+ const a = story.find('.story__title a');
+ fixImage(story);
+ story.find('.player').each((_, elem) => {
+ elem = $(elem);
+ fixVideo(elem);
+ });
+ return {
+ title: a.text(),
+ link: a.attr('href'),
+ pubDate: parseDate(story.find('time').attr('datetime')),
+ description: story.find('.story__content-inner').html(),
+ author: story.find('.user__nick').text(),
+ };
+ });
+
+ ctx.state.data = {
+ title: $('meta[property="og:title"]').attr('content'),
+ description: $('.profile__user-about-content').text(),
+ image: $('meta[property="og:image"]').attr('content'),
+ language: 'ru-RU',
+ link,
+ item: items,
+ };
+};
diff --git a/lib/v2/pikabu/utils.js b/lib/v2/pikabu/utils.js
new file mode 100644
index 000000000..85b8a87c8
--- /dev/null
+++ b/lib/v2/pikabu/utils.js
@@ -0,0 +1,43 @@
+const { art } = require('@/utils/render');
+const path = require('path');
+
+const baseUrl = 'https://pikabu.ru';
+
+const fixImage = (element) => {
+ element.find('.story-image__stretch').remove();
+ element.find('.story-image__image').each((_, img) => {
+ if (img.attribs['data-src'] && img.attribs['data-large-image']) {
+ img.attribs.src = img.attribs['data-large-image'];
+ delete img.attribs['data-src'];
+ delete img.attribs['data-large-image'];
+ }
+ });
+};
+
+const fixVideo = (element) => {
+ const preview = element
+ .find('.player__preview')
+ .attr('style')
+ .match(/url\((.+)\);/)[1];
+ const dataType = element.attr('data-type');
+ let videoHtml = '';
+
+ if (dataType === 'video') {
+ const videoId = element.attr('data-source').match(/\/embed\/(.+)$/)[1];
+ videoHtml = art(path.join(__dirname, 'templates/video.art'), { videoId });
+ } else if (dataType === 'video-file') {
+ const width = element.find('.player__svg-stretch').attr('width');
+ const mp4 = `${element.attr('data-source')}.mp4`;
+ const webm = element.attr('data-webm');
+ videoHtml = art(path.join(__dirname, 'templates/video.art'), { preview, width, mp4, webm });
+ } else {
+ throw Error(`Unknown video type: ${dataType}`);
+ }
+ element.replaceWith(videoHtml);
+};
+
+module.exports = {
+ baseUrl,
+ fixImage,
+ fixVideo,
+};