feat: pixiv ranking

This commit is contained in:
eyhn
2018-04-25 21:24:13 +08:00
parent 8fca0b6f1c
commit 8d13b9d3b6
6 changed files with 219 additions and 96 deletions

View File

@@ -59,6 +59,9 @@ router.get('/mzitu/tag/:tag', require('./routes/mzitu/tag'));
// // Pixiv // // Pixiv
router.get('/pixiv/user/:id', require('./routes/pixiv/user')); router.get('/pixiv/user/:id', require('./routes/pixiv/user'));
// mode: [ day, week, month, day_male, day_female, week_original, week_rookie, day_r18, day_male_r18, day_female_r18, week_r18, week_r18g]
// date: 2018-4-25
router.get('/pixiv/ranking/:mode/:date?', require('./routes/pixiv/ranking'));
// 豆瓣 // 豆瓣
router.get('/douban/movie/playing', require('./routes/douban/playing')); router.get('/douban/movie/playing', require('./routes/douban/playing'));

View File

@@ -0,0 +1,8 @@
exports.modules = {
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)'
}
};

106
routes/pixiv/ranking.js Normal file
View File

@@ -0,0 +1,106 @@
const config = require('../../config');
const pixivConfig = config.pixiv;
if (!pixivConfig) {
logger.info('Pixiv RSS disable.');
module.exports = () => {};
return;
}
const axios = require('axios');
const art = require('art-template');
const path = require('path');
const FormData = require('form-data');
const getToken = require('./token');
const maskHeader = require('./constants').maskHeader;
const assert = require('assert');
const util = require('util');
const allowMode = [
'day', 'week', 'month', 'day_male', 'day_female', 'week_original', 'week_rookie', 'day_r18', 'day_male_r18', 'day_female_r18', 'week_r18', 'week_r18g'
];
const titles = {
'day': 'pixiv 日排行',
'week': 'pixiv 周排行',
'month': 'pixiv 月排行',
'day_male': 'pixiv 受男性欢迎排行',
'day_female': 'pixiv 受女性欢迎排行',
'week_original': 'pixiv 原创作品排行',
'week_rookie': 'pixiv 新人排行',
'day_r18': 'pixiv R-18 日排行',
'day_male_r18': 'pixiv R-18 受男性欢迎排行',
'day_female_r18': 'pixiv R-18 受女性欢迎排行',
'week_r18': 'pixiv R-18 周排行',
'week_r18g': 'pixiv R-18G 排行'
};
const links = {
'day': 'https://www.pixiv.net/ranking.php?mode=daily',
'week': 'https://www.pixiv.net/ranking.php?mode=weekly',
'month': 'https://www.pixiv.net/ranking.php?mode=monthly',
'day_male': 'https://www.pixiv.net/ranking.php?mode=male',
'day_female': 'https://www.pixiv.net/ranking.php?mode=female',
'week_original': 'https://www.pixiv.net/ranking.php?mode=original',
'week_rookie': 'https://www.pixiv.net/ranking.php?mode=rookie',
'day_r18': 'https://www.pixiv.net/ranking.php?mode=daily_r18',
'day_male_r18': 'https://www.pixiv.net/ranking.php?mode=male_r18',
'day_female_r18': 'https://www.pixiv.net/ranking.php?mode=female_r18',
'week_r18': 'https://www.pixiv.net/ranking.php?mode=weekly_r18',
'week_r18g': 'https://www.pixiv.net/ranking.php?mode=r18g'
};
module.exports = async (ctx) => {
const mode = ctx.params.mode;
const date = ctx.params.date ? new Date(ctx.params.date) : new Date();
assert(allowMode.includes(mode), 'Mode not allow.');
if (typeof getToken() === 'null') {
ctx.throw(500);
return;
}
const response = await axios({
method: 'get',
url: 'https://app-api.pixiv.net/v1/illust/ranking',
headers: {
...maskHeader,
'Authorization': 'Bearer ' + getToken()
},
params: {
mode: mode,
filter: 'for_ios',
...(ctx.params.date && {
date: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
})
},
});
const illusts = response.data.illusts;
const dateStr = `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`;
ctx.body = art(path.resolve(__dirname, '../../views/rss.art'), {
title: (ctx.params.date ? dateStr : '') + titles[mode],
link: links[mode],
description: dateStr + titles[mode],
lastBuildDate: new Date().toUTCString(),
item: illusts.map((illust, index) => {
const images = [];
if (illust.page_count === 1) {
images.push(`<p><img referrerpolicy="no-referrer" src="https://pixiv.cat/${illust.id}.jpg"/></p>`);
} else {
for (let i = 0; i < illust.page_count; i++) {
images.push(`<p><img referrerpolicy="no-referrer" src="https://pixiv.cat/${illust.id}-${i+1}.jpg"/></p>`);
}
}
return {
title: `#${index + 1} ${illust.title}`,
description: `<p>画师:${illust.user.name} - 上传于:${new Date(illust.create_date).toLocaleString('zh-cn')} - 阅览数:${illust.total_view} - 收藏数:${illust.total_bookmarks}</p>${images.join('')}`,
link: `https://www.pixiv.net/member_illust.php?mode=medium&illust_id=${illust.id}`
};
})
});
};

89
routes/pixiv/token.js Normal file
View File

@@ -0,0 +1,89 @@
const config = require('../../config');
const logger = require('../../utils/logger');
const wait = require('../../utils/wait');
const FormData = require('form-data');
const axios = require('axios');
const maskHeader = require('./constants').maskHeader;
const pixivConfig = config.pixiv;
let token = null;
const authorizationInfo = {
client_id: pixivConfig.client_id,
client_secret: pixivConfig.client_secret,
username: pixivConfig.username,
password: pixivConfig.password
};
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;
}
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 = function getToken() {
return token;
};

View File

@@ -1,9 +1,4 @@
const axios = require('axios');
const art = require('art-template');
const path = require('path');
const config = require('../../config'); const config = require('../../config');
const logger = require('../../utils/logger');
const FormData = require('form-data');
const pixivConfig = config.pixiv; const pixivConfig = config.pixiv;
@@ -13,100 +8,17 @@ if (!pixivConfig) {
return; return;
} }
const authorizationInfo = { const axios = require('axios');
client_id: pixivConfig.client_id, const art = require('art-template');
client_secret: pixivConfig.client_secret, const path = require('path');
username: pixivConfig.username, const FormData = require('form-data');
password: pixivConfig.password const getToken = require('./token');
}; const maskHeader = require('./constants').maskHeader;
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) => { module.exports = async (ctx) => {
const id = ctx.params.id; const id = ctx.params.id;
if (typeof token === 'null') { if (typeof getToken() === 'null') {
ctx.throw(500); ctx.throw(500);
return; return;
} }
@@ -116,7 +28,7 @@ module.exports = async (ctx) => {
url: 'https://app-api.pixiv.net/v1/user/illusts', url: 'https://app-api.pixiv.net/v1/user/illusts',
headers: { headers: {
...maskHeader, ...maskHeader,
'Authorization': 'Bearer ' + token 'Authorization': 'Bearer ' + getToken()
}, },
params: { params: {
user_id: id, user_id: id,

5
utils/wait.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}