From 165954ccf84d725b1f2790e163f4c0bbb8da1f0e Mon Sep 17 00:00:00 2001 From: sgqy Date: Sat, 29 Aug 2020 05:13:01 +0800 Subject: [PATCH] feat: add pixiv-fanbox (#5532) --- docs/en/social-media.md | 12 ++ docs/social-media.md | 11 ++ lib/config.js | 3 + lib/router.js | 3 + lib/routes/fanbox/conv.js | 227 ++++++++++++++++++++++++++++++++++++ lib/routes/fanbox/header.js | 13 +++ lib/routes/fanbox/main.js | 42 +++++++ 7 files changed, 311 insertions(+) create mode 100644 lib/routes/fanbox/conv.js create mode 100644 lib/routes/fanbox/header.js create mode 100644 lib/routes/fanbox/main.js diff --git a/docs/en/social-media.md b/docs/en/social-media.md index 4a1be53abe..b850c1e1ae 100644 --- a/docs/en/social-media.md +++ b/docs/en/social-media.md @@ -115,6 +115,18 @@ Only for self-hosted ::: +## pixiv-fanbox + + +::: warning Paywall + +The site **fanbox** is made for paid users. To get paid content, you should host by yourself. + +Settings: environment argument `FANBOX_SESSION_ID` equals to `FANBOXSESSID` in site cookies. + +::: + + ## Telegram ### Channel diff --git a/docs/social-media.md b/docs/social-media.md index 959707a438..d060ec91db 100644 --- a/docs/social-media.md +++ b/docs/social-media.md @@ -403,6 +403,17 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性 ::: +## pixiv-fanbox + + +::: warning 付费墙注意 + +由于 fanbox 专为付费用户设计,要查看已经付费的内容只能自建。 + +自建设置:环境变量`FANBOX_SESSION_ID`对应 cookies 中的`FANBOXSESSID`。 + +::: + ## Popi 提问箱 ### 提问箱新回答 diff --git a/lib/config.js b/lib/config.js index 969e94138d..f4762b2673 100644 --- a/lib/config.js +++ b/lib/config.js @@ -56,6 +56,9 @@ const calculateValue = () => { username: envs.PIXIV_USERNAME, password: envs.PIXIV_PASSWORD, }, + fanbox: { + session: envs.FANBOX_SESSION_ID, + }, disqus: { api_key: envs.DISQUS_API_KEY, }, diff --git a/lib/router.js b/lib/router.js index ca25e9ce92..d36c8256df 100644 --- a/lib/router.js +++ b/lib/router.js @@ -162,6 +162,9 @@ router.get('/pixiv/user/:id', require('./routes/pixiv/user')); router.get('/pixiv/ranking/:mode/:date?', require('./routes/pixiv/ranking')); router.get('/pixiv/search/:keyword/:order?/:r18?', require('./routes/pixiv/search')); +// pixiv-fanbox +router.get('/fanbox/:user?', require('./routes/fanbox/main')); + // 豆瓣 router.get('/douban/movie/playing', require('./routes/douban/playing')); router.get('/douban/movie/playing/:score', require('./routes/douban/playing')); diff --git a/lib/routes/fanbox/conv.js b/lib/routes/fanbox/conv.js new file mode 100644 index 0000000000..d85c936385 --- /dev/null +++ b/lib/routes/fanbox/conv.js @@ -0,0 +1,227 @@ +const got = require('@/utils/got'); + +const get_header = require('./header'); + +async function get_twitter(t) { + try { + const resp = await got(`https://publish.twitter.com/oembed?url=${t}`); + return resp.data.html; + } catch (_) { + return `
This tweet may not exist
`; + } +} + +async function get_fanbox(p) { + try { + const m = p.match(/creator\/([0-9]+)\/post\/([0-9]+)/); + const post_id = m[2]; + const api_url = `https://api.fanbox.cc/post.info?postId=${post_id}`; + const resp = await got(api_url, { headers: get_header() }); + const post = resp.data.body; + + const home_url = `https://${post.creatorId}.fanbox.cc`; + const web_url = `${home_url}/posts/${post.id}`; + const datetime = new Date(post.updatedDatetime).toLocaleString('ja'); + + const box_html = ` +
+ +
+ ${post.title} +
+
+ ${post.user.name} + Modify: ${datetime} + ${post.feeRequired} JPY +
+ `; + return { url: web_url, html: box_html }; + } catch (_) { + return { url: null, html: `
fanbox post (${p}) may not exist
` }; + } +} + +// embedded items +async function embed_map(e) { + const id = e.contentId || e.videoId; + const sp = e.serviceProvider; + + let ret = `Unknown host: ${sp}, with ID: ${id}`; + let url = null; + + try { + switch (sp) { + case 'youtube': + url = `https://www.youtube.com/embed/${id}`; + ret = ``; + break; + case 'vimeo': + url = `https://player.vimeo.com/video/${id}`; + ret = ``; + break; + case 'soundcloud': + url = `https://soundcloud.com/${id}`; + ret = ``; + break; + case 'twitter': + url = `https://twitter.com/i/status/${id}`; + ret = await get_twitter(url); + break; + case 'google_forms': + url = `https://docs.google.com/forms/d/e/${id}/viewform?embedded=true`; + ret = ``; + break; + case 'fanbox': { + const info = await get_fanbox(id); + url = info.url; + ret = info.html; + break; + } + case 'gist': + url = `https://gist.github.com/${id}`; + ret = ``; + break; + } + if (url) { + ret += `
Click here if embedded content is not loaded.`; + } + } catch (_) { + _; + } + + return ret; +} + +// render

blocks +function passage_conv(p) { + const seg = p.text.split(''); + // seg.push(''); + if (p.styles) { + p.styles.map((s) => { + switch (s.type) { + case 'bold': + seg[s.offset] = `` + seg[s.offset]; + seg[s.offset + s.length - 1] += ``; + break; + } + return s; + }); + } + if (p.links) { + p.links.map((l) => { + seg[l.offset] = `` + seg[l.offset]; + seg[l.offset + l.length - 1] += ``; + return l; + }); + } + const ret = seg.join(''); + // console.log(ret) + return ret; +} + +// article types +function text_t(body) { + return body.text || ''; +} + +function image_t(body) { + let ret = body.text || ''; + body.images.map((i) => (ret += `


`)); + return ret; +} + +function file_t(body) { + let ret = body.text || ''; + body.files.map((f) => (ret += `
${f.name}.${f.extension}`)); + return ret; +} + +async function video_t(body) { + let ret = body.text || ''; + ret += (await embed_map(body.video)) || ''; + return ret; +} + +async function blog_t(body) { + let ret = []; + for (let x = 0; x < body.blocks.length; ++x) { + const b = body.blocks[x]; + ret.push('

'); + + switch (b.type) { + case 'p': + ret.push(passage_conv(b)); + break; + case 'header': + ret.push(`

${b.text}

`); + break; + case 'image': { + const i = body.imageMap[b.imageId]; + ret.push(``); + break; + } + case 'file': { + const f = body.fileMap[b.fileId]; + ret.push(`${f.name}.${f.extension}`); + break; + } + case 'embed': + ret.push(embed_map(body.embedMap[b.embedId])); // Promise object + break; + } + } + ret = await Promise.all(ret); // get real data + return ret.join(''); +} + +// parse by type +async function conv_article(i) { + let ret = ''; + if (i.title) { + ret += `[${i.type}] ${i.title}
`; + } + if (i.feeRequired !== 0) { + ret += `Fee Required: ${i.feeRequired} JPY/month
`; + } + if (i.coverImageUrl) { + ret += `
`; + } + + if (!i.body) { + ret += `${i.excerpt}`; + return ret; + } + + // console.log(i); + // skip paywall + + switch (i.type) { + case 'text': + ret += text_t(i.body); + break; + case 'file': + ret += file_t(i.body); + break; + case 'image': + ret += image_t(i.body); + break; + case 'video': + ret += await video_t(i.body); + break; + case 'article': + ret += await blog_t(i.body); + break; + default: + ret += 'Unsupported content (RSSHub)'; + } + return ret; +} + +// render wrapper +module.exports = async (i) => ({ + title: i.title || `No title`, + description: await conv_article(i), + pubDate: new Date(i.publishedDatetime).toUTCString(), + link: `https://${i.creatorId}.fanbox.cc/posts/${i.id}`, + category: i.tags, +}); diff --git a/lib/routes/fanbox/header.js b/lib/routes/fanbox/header.js new file mode 100644 index 0000000000..6486b9b965 --- /dev/null +++ b/lib/routes/fanbox/header.js @@ -0,0 +1,13 @@ +const config = require('@/config').value; + +// unlock contents paid by user +module.exports = () => { + const sessid = config.fanbox.session; + let cookie = ''; + if (sessid) { + cookie += `FANBOXSESSID=${sessid}`; + } + const headers = { origin: 'https://fanbox.cc', cookie: cookie }; + + return headers; +}; diff --git a/lib/routes/fanbox/main.js b/lib/routes/fanbox/main.js new file mode 100644 index 0000000000..5149eaf6ba --- /dev/null +++ b/lib/routes/fanbox/main.js @@ -0,0 +1,42 @@ +// pixiv fanbox, maybe blocked by upstream + +// params: +// user?: fanbox domain name + +const got = require('@/utils/got'); + +const conv_item = require('./conv'); +const get_header = require('./header'); + +module.exports = async (ctx) => { + const user = ctx.params.user || 'official'; // if no user specified, just go to official page + const box_url = `https://${user}.fanbox.cc`; + + // get user info + let title = `${user}'s fanbox`; + let descr = title; + + try { + const user_api = `https://api.fanbox.cc/creator.get?creatorId=${user}`; + const resp_u = await got(user_api, { headers: get_header() }); + title = `${resp_u.data.body.user.name}'s fanbox`; + descr = resp_u.data.description; + } catch (_) { + _; + } + + // get user posts + const posts_api = `https://api.fanbox.cc/post.listCreator?creatorId=${user}&limit=20`; + const response = await got(posts_api, { headers: get_header() }); + + // render posts + const items = await Promise.all(response.data.body.items.map((i) => conv_item(i))); + + // return rss feed + ctx.state.data = { + title: title, + link: box_url, + description: descr, + item: items, + }; +};