diff --git a/docs/social-media.md b/docs/social-media.md
index 635b1f7b24..ebe1f0deae 100644
--- a/docs/social-media.md
+++ b/docs/social-media.md
@@ -312,6 +312,16 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
+## piapro
+
+### 用户最新作品
+
+
+
+### 全站最新作品
+
+
+
## pixiv
### 用户收藏
diff --git a/lib/router.js b/lib/router.js
index e17e13c083..f12c45657f 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -2187,4 +2187,8 @@ router.get('/netease/ds/:id', require('./routes/netease/ds'));
// javlibrary
router.get('/javlibrary/userposts/:uid', require('./routes/javlibrary/userposts'));
+// piapro
+router.get('/piapro/user/:pid', require('./routes/piapro/user'));
+router.get('/piapro/public/:type/:tag?/:category?', require('./routes/piapro/public'));
+
module.exports = router;
diff --git a/lib/routes/piapro/public.js b/lib/routes/piapro/public.js
new file mode 100644
index 0000000000..2c25f0920a
--- /dev/null
+++ b/lib/routes/piapro/public.js
@@ -0,0 +1,62 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const utils = require('./utils');
+
+const base = 'https://piapro.jp';
+
+const selectors = {
+ music: '.snd_list li .title',
+ illust: '#itemcont .i_main',
+ text: '.txt_list li .title',
+};
+
+module.exports = async (ctx) => {
+ const type = selectors[ctx.params.type] ? ctx.params.type : 'music';
+ const tag = ctx.params.tag ? ctx.params.tag : '';
+ const category = ctx.params.category ? ctx.params.category : '';
+
+ const url = `${base}/${type}/?tag=${encodeURIComponent(tag)}&categoryId=${category}`;
+
+ const list_response = await got.get(url);
+ const $ = cheerio.load(list_response.data);
+
+ const type_name = $('#top_titlebox img')
+ .first()
+ .attr('alt');
+ const category_name = $('#_filterbox_category .name').text();
+ const tag_name = $('#_filterbox_character .name').text();
+
+ const list = $(selectors[type])
+ .map(function() {
+ return (
+ base +
+ $(this)
+ .find('a')
+ .first()
+ .attr('href')
+ );
+ })
+ .toArray();
+
+ const out = await Promise.all(
+ list.slice(0, 10).map(async (url) => {
+ const cache = await ctx.cache.get(url);
+ if (cache) {
+ return Promise.resolve(JSON.parse(cache));
+ }
+
+ const response = await got.get(url);
+ const item = utils.parseContent(response.data);
+ item.link = url;
+
+ ctx.cache.set(url, JSON.stringify(item));
+ return Promise.resolve(item);
+ })
+ );
+
+ ctx.state.data = {
+ title: `piapro - ${type_name}${tag ? ' - ' + tag_name : ''}${category ? ' - ' + category_name : ''}`,
+ link: url,
+ item: out.filter((item) => item !== ''),
+ };
+};
diff --git a/lib/routes/piapro/user.js b/lib/routes/piapro/user.js
new file mode 100644
index 0000000000..221ba0e36e
--- /dev/null
+++ b/lib/routes/piapro/user.js
@@ -0,0 +1,53 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const utils = require('./utils');
+
+const base = 'https://piapro.jp';
+
+module.exports = async (ctx) => {
+ const pid = ctx.params.pid;
+
+ const url = `${base}/my_page/?view=content&pid=${pid}`;
+
+ const list_response = await got.get(url);
+
+ const $ = cheerio.load(list_response.data);
+
+ const user_name = $('#main_name h2').text();
+ const user_description = $('#main_name p').text();
+
+ const list = $('#itemcont .i_main')
+ .map(function() {
+ return (
+ base +
+ $(this)
+ .find('a')
+ .last()
+ .attr('href')
+ );
+ })
+ .toArray();
+
+ const out = await Promise.all(
+ list.slice(0, 10).map(async (url) => {
+ const cache = await ctx.cache.get(url);
+ if (cache) {
+ return Promise.resolve(JSON.parse(cache));
+ }
+
+ const response = await got.get(url);
+ const item = utils.parseContent(response.data);
+ item.link = url;
+
+ ctx.cache.set(url, JSON.stringify(item));
+ return Promise.resolve(item);
+ })
+ );
+
+ ctx.state.data = {
+ title: `piapro - ${user_name}の投稿作品`,
+ description: user_description,
+ link: url,
+ item: out.filter((item) => item !== ''),
+ };
+};
diff --git a/lib/routes/piapro/utils.js b/lib/routes/piapro/utils.js
new file mode 100644
index 0000000000..993b419848
--- /dev/null
+++ b/lib/routes/piapro/utils.js
@@ -0,0 +1,45 @@
+const cheerio = require('cheerio');
+
+const parseContent = (htmlString) => {
+ const $ = cheerio.load(htmlString);
+
+ const title = $('h1.cd_works-title');
+ const author = $('.cd_user-box-main a.cd_user-name');
+ const time = $('.cd_dtl_data')
+ .text()
+ .match(/\d+\/\d+\/\d+ \d+:\d+:\d+/)[0];
+
+ const blank = cheerio.load('
');
+ const content = blank('div');
+
+ // description
+ $('p.cd_dtl_cap').appendTo(content);
+
+ // image
+ $('.illust-whole').appendTo(content);
+
+ // audio
+ const testAudio = htmlString.match(/initNotLoginAudioFunc\(([\s\S]*?)\);/m);
+ if (testAudio) {
+ const audioInfo = testAudio[1];
+ const contentId = audioInfo.match(/contentId:\s*'(.*?)',/)[1];
+ const createDate = audioInfo.match(/createDate:\s*'(.*?)',/)[1];
+ const audio_url = `https://cdn.piapro.jp/mp3_a/${contentId.substring(0, 2)}/${contentId}_${createDate}_audition.mp3`;
+ $(``).appendTo(content);
+ $('img#bigJacket_1').appendTo(content);
+ }
+
+ // text
+ $('.main_txt').appendTo(content);
+
+ return {
+ title: title.text().trim(),
+ author: author.text().trim(),
+ description: content.html(),
+ pubDate: new Date(time),
+ };
+};
+
+module.exports = {
+ parseContent,
+};