diff --git a/README.md b/README.md index b76f929b83..a908782cec 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ RSSHub 是一个轻量、易于扩展的 RSS 生成器,可以给任何奇奇 - 所有专题 - 专题详情 - 详情 +- Pixiv + - 用户动态 ## 参与我们 diff --git a/config.js b/config.js index fae8ebdd2b..af32ca1e4c 100644 --- a/config.js +++ b/config.js @@ -3,4 +3,10 @@ module.exports = { cacheType: 'memory', // support memory and redis, set empty to disable cache cacheExpire: 5 * 60, ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36', + // pixiv: { + // client_id: 'XXXXXXXXXXXXXXXXXXX', + // client_secret: 'XXXXXXXXXXXXXXXXX', + // username: 'xxxx@xxx.xx', + // password: 'xxxxx' + // } }; \ No newline at end of file diff --git a/package.json b/package.json index e25132f1ad..68efd306c3 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "cheerio": "1.0.0-rc.2", "crypto": "1.0.1", "eslint": "4.19.1", + "form-data": "^2.3.2", "koa": "2.5.0", "koa-redis-cache": "3.0.0", "koa-router": "7.4.0", diff --git a/router.js b/router.js index 1ccc208731..1cbe2c7463 100644 --- a/router.js +++ b/router.js @@ -57,4 +57,7 @@ router.get('/mzitu/category/:category', require('./routes/mzitu/category')); router.get('/mzitu/post/:id', require('./routes/mzitu/post')); router.get('/mzitu/tag/:tag', require('./routes/mzitu/tag')); +// // Pixiv +router.get('/pixiv/user/:id', require('./routes/pixiv/user')); + module.exports = router; diff --git a/routes/pixiv/user.js b/routes/pixiv/user.js new file mode 100644 index 0000000000..cc7da5ad01 --- /dev/null +++ b/routes/pixiv/user.js @@ -0,0 +1,152 @@ +const axios = require('axios'); +const art = require('art-template'); +const path = require('path'); +const config = require('../../config'); +const logger = require('../../utils/logger'); +const FormData = require('form-data'); + +const pixivConfig = config.pixiv; + +if (!pixivConfig) { + logger.info('Pixiv RSS disable.'); + module.exports = () => {}; + return; +} + +const authorizationInfo = { + client_id: pixivConfig.client_id, + client_secret: pixivConfig.client_secret, + username: pixivConfig.username, + password: pixivConfig.password +}; + +const maskHeader = { + 'App-OS': 'ios', + 'App-OS-Version': '10.3.1', + 'App-Version': '6.7.1', + 'User-Agent': 'PixivIOSApp/6.7.1 (iOS 10.3.1; iPhone8,1)' +}; + +let token = null; + +async function getToken() { + const data = new FormData(); + const jsonData = { + ...authorizationInfo, + get_secure_url: 1, + grant_type: 'password' + }; + for (const key in jsonData) { + if (jsonData.hasOwnProperty(key)) { + const element = jsonData[key]; + data.append(key,element); + } + } + const response = await axios.post('https://oauth.secure.pixiv.net/auth/token', data, { + headers: { + ...maskHeader, + ...data.getHeaders() + } + }); + return response.data.response; +} + +async function refreshToken(refresh_token) { + const data = new FormData(); + const jsonData = { + ...authorizationInfo, + get_secure_url: 1, + grant_type: 'refresh_token', + refresh_token: refresh_token + }; + for (const key in jsonData) { + if (jsonData.hasOwnProperty(key)) { + const element = jsonData[key]; + data.append(key,element); + } + } + const response = await axios.post('https://oauth.secure.pixiv.net/auth/token', data, { + headers: { + ...maskHeader, + ...data.getHeaders() + } + }); + return response.data.response; +} + +function wait(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +async function tokenLoop() { + const res = await getToken(); + logger.info('Pixiv login success.'); + token = res.access_token; + let refresh_token = res.refresh_token; + let expires_in = res.expires_in * 0.9; + while (true) { + await wait(expires_in * 1000); + try { + const refresh_res = await refreshToken(refresh_token); + logger.debug('Pixiv refresh token success.'); + token = refresh_res.access_token; + refresh_token = refresh_res.refresh_token; + expires_in = refresh_res.expires_in * 0.9; + } catch (err) { + expires_in = 30; + logger.err('Pixiv refresh token failed, retry in ${expires_in} seconds.', err); + } + } +} + +tokenLoop(); + +module.exports = async (ctx) => { + const id = ctx.params.id; + + if (typeof token === 'null') { + ctx.throw(500); + return; + } + + const response = await axios({ + method: 'get', + url: 'https://app-api.pixiv.net/v1/user/illusts', + headers: { + ...maskHeader, + 'Authorization': 'Bearer ' + token + }, + params: { + user_id: id, + filter: 'for_ios', + type: 'illust' + }, + }); + + const illusts = response.data.illusts; + const username = illusts[0].user.name + + ctx.body = art(path.resolve(__dirname, '../../views/rss.art'), { + title: `${username} 的 Pixiv 动态`, + link: `https://music.163.com/#/playlist?id=${id}`, + description: `${username} 的 Pixiv 最新动态`, + lastBuildDate: new Date().toUTCString(), + item: illusts.map((illust) => { + const images = []; + if (illust.page_count === 1) { + images.push(`

`); + } else { + for (let i = 0; i < illust.page_count; i++) { + images.push(`

`); + } + } + return { + title: illust.title, + description: `

画师:${username} - 上传于:${new Date(illust.create_date).toLocaleString('zh-cn')} - 阅览数:${illust.total_view} - 收藏数:${illust.total_bookmarks}

${images.join('')}`, + link: `https://www.pixiv.net/member_illust.php?mode=medium&illust_id=${illust.id}` + }; + }) + }); +}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5aaedac3ae..e9c1accc91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,6 +106,10 @@ async@^2.6.0: dependencies: lodash "^4.14.0" +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + axios@0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" @@ -262,6 +266,12 @@ colorspace@1.0.x: color "0.8.x" text-hex "0.0.x" +combined-stream@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + commander@2.15.x, commander@~2.15.0: version "2.15.1" resolved "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" @@ -353,6 +363,10 @@ del@^2.0.2: pinkie-promise "^2.0.0" rimraf "^2.2.8" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + delegates@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -618,6 +632,14 @@ follow-redirects@^1.3.0: dependencies: debug "^3.1.0" +form-data@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + fresh@^0.5.2: version "0.5.2" resolved "http://registry.npm.taobao.org/fresh/download/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -957,7 +979,7 @@ mime-db@~1.33.0: version "1.33.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" -mime-types@^2.0.7, mime-types@~2.1.18: +mime-types@^2.0.7, mime-types@^2.1.12, mime-types@~2.1.18: version "2.1.18" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" dependencies: