Merge remote-tracking branch 'DIYgod/master' into dev

This commit is contained in:
Henry 2019-01-30 09:31:03 +00:00
commit 47778c187d
29 changed files with 1390 additions and 779 deletions

View File

@ -1,3 +1,4 @@
comment: off
coverage:
status:
patch:

View File

@ -297,6 +297,8 @@ RSSHub 提供下列 API 接口:
<route name="视频评论" author="Qixingchen" example="/bilibili/video/reply/21669336" path="/bilibili/video/reply/:aid" :paramsDesc="['可在视频页 URL 中找到']"/>
<route name="视频弹幕" author="Qixingchen" example="/bilibili/video/danmaku/21669336/1" path="/bilibili/video/danmaku/:aid/:pid?" :paramsDesc="['视频AV号,可在视频页 URL 中找到','分P号,不填默认为1']"/>
<route name="link 公告" author="Qixingchen" example="/bilibili/link/news/live" path="/bilibili/link/news/:product" :paramsDesc="['公告分类, 包括 直播:live 小视频:vc 相簿:wh']"/>
#### 直播开播 <Author uid="Qixingchen"/>
@ -497,6 +499,8 @@ RSSHub 提供下列 API 接口:
</route>
<route name="豆列" author="LogicJake" example="/douban/doulist/37716774" path="douban/doulist/:id" :paramsDesc="['豆列id']"/>
### Disqus
<route name="评论" author="DIYgod" example="/disqus/posts/diygod-me" path="/disqus/posts/:forum" :paramsDesc="['网站的 disqus name']"/>
@ -2778,23 +2782,23 @@ board 和 build 可在[这里](http://api.ineal.me/tss/status)查看
### JavBus
<route name="首页" author="MegrezZhu" example="/javbus/home" path="/javbus/home" />
<route name="首页" author="MegrezZhu CoderTonyChan" example="/javbus/home" path="/javbus/home"/>
<route name="分类" author="MegrezZhu" example="/javbus/genre/7g" path="/javbus/genre/:gid" :paramsDesc="['分类id详见[网站里](https://www.javbus.com/genre)的链接']" />
<route name="分类" author="MegrezZhu CoderTonyChan" example="/javbus/genre/7g" path="/javbus/genre/:gid" :paramsDesc="['分类id详见[网站里](https://www.javbus.com/genre)的链接']" />
<route name="演员" author="MegrezZhu" example="/javbus/star/2jv" path="/javbus/star/:sid" :paramsDesc="['演员id详见[网站里](https://www.javbus.com/actresses)的链接']" />
<route name="演员" author="MegrezZhu CoderTonyChan" example="/javbus/star/2jv" path="/javbus/star/:sid" :paramsDesc="['演员id详见[网站里](https://www.javbus.com/actresses)的链接']" />
<route name="首页/步兵" author="MegrezZhu" example="/javbus/uncensored/home" path="/javbus/uncensored/home" />
<route name="首页/步兵" author="MegrezZhu CoderTonyChan" example="/javbus/uncensored/home" path="/javbus/uncensored/home"/>
<route name="分类/步兵" author="MegrezZhu" example="/javbus/uncensored/genre/1bc" path="/javbus/uncensored/genre/:gid" :paramsDesc="['分类id详见[网站里](https://www.javbus.com/uncensored/genre)的链接']" />
<route name="分类/步兵" author="MegrezZhu CoderTonyChan" example="/javbus/uncensored/genre/1bc" path="/javbus/uncensored/genre/:gid" :paramsDesc="['分类id详见[网站里](https://www.javbus.com/uncensored/genre)的链接']" />
<route name="演员/步兵" author="MegrezZhu" example="/javbus/uncensored/star/b5b" path="/javbus/uncensored/star/:sid" :paramsDesc="['演员id详见[网站里](https://www.javbus.com/uncensored/actresses)的链接']" />
<route name="演员/步兵" author="MegrezZhu CoderTonyChan" example="/javbus/uncensored/star/b5b" path="/javbus/uncensored/star/:sid" :paramsDesc="['演员id详见[网站里](https://www.javbus.com/uncensored/actresses)的链接']" />
<route name="首页/欧陆风云" author="MegrezZhu" example="/javbus/western/home" path="/javbus/western/home" />
<route name="首页/欧陆风云" author="MegrezZhu CoderTonyChan" example="/javbus/western/home" path="/javbus/western/home"/>
<route name="分类/欧陆风云" author="MegrezZhu" example="/javbus/western/genre/86" path="/javbus/western/genre/:gid" :paramsDesc="['分类id详见[网站里](https://www.javbus.work/genre)的链接']" />
<route name="分类/欧陆风云" author="MegrezZhu CoderTonyChan" example="/javbus/western/genre/86" path="/javbus/western/genre/:gid" :paramsDesc="['分类id详见[网站里](https://www.javbus.work/genre)的链接']" />
<route name="演员/欧陆风云" author="MegrezZhu" example="/javbus/western/star/4hv" path="/javbus/western/star/:sid" :paramsDesc="['演员id详见[网站里](https://www.javbus.work/actresses)的链接']" />
<route name="演员/欧陆风云" author="MegrezZhu CoderTonyChan" example="/javbus/western/star/4hv" path="/javbus/western/star/:sid" :paramsDesc="['演员id详见[网站里](https://www.javbus.work/actresses)的链接']" />
### 虎嗅
@ -2802,6 +2806,8 @@ board 和 build 可在[这里](http://api.ineal.me/tss/status)查看
<route name="搜索" author="xyqfer" example="/huxiu/search/%E8%99%8E%E5%97%85%E6%97%A9%E6%8A%A5" path="/huxiu/search/:keyword" :paramsDesc="['关键字']" />
<route name="作者" author="HenryQW" example="/huxiu/author/29318" path="/huxiu/author/:id" :paramsDesc="['用户 id']" />
### 扇贝
<route name="打卡" author="DIYgod" example="/shanbay/checkin/ddwej" path="/shanbay/checkin/:id" :paramsDesc="['用户 id']" />
@ -2833,3 +2839,14 @@ board 和 build 可在[这里](http://api.ineal.me/tss/status)查看
### 移动支付网
<route name="新闻" author="LogicJake" example="/mpaypass/news" path="/mpaypass/news"/>
### 新浪科技
<route name="科学探索" author="LogicJake" example="/sina/discovery/zx" path="/sina/discovery/:type" :paramsDesc="['订阅分区类型']">
分类:
| zx | twhk | dwzw | zrdl | lskg | smyx | shbk | kjqy |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| 最新 | 天文航空 | 动物植物 | 自然地理 | 历史考古 | 生命医学 | 生活百科 | 科技前沿 |
</route>

View File

@ -37,29 +37,29 @@ function responseHandler() {
ctx.body = { status, data, message };
};
ctx.res.fail = ({ statusCode, code, data = null, message = null }) => {
const status = -1;
// ctx.res.fail = ({ statusCode, code, data = null, message = null }) => {
// const status = -1;
if (!!statusCode && (statusCode >= 400 && statusCode < 500)) {
ctx.status = statusCode;
} else if (!(ctx.status >= 400 && ctx.status < 500)) {
ctx.status = statusCodes.BAD_REQUEST;
}
// if (!!statusCode && (statusCode >= 400 && statusCode < 500)) {
// ctx.status = statusCode;
// } else if (!(ctx.status >= 400 && ctx.status < 500)) {
// ctx.status = statusCodes.BAD_REQUEST;
// }
ctx.body = { status, code, data, message };
};
// ctx.body = { status, code, data, message };
// };
ctx.res.error = ({ statusCode, code, data = null, message = null }) => {
const status = -2;
// ctx.res.error = ({ statusCode, code, data = null, message = null }) => {
// const status = -2;
if (!!statusCode && (statusCode >= 500 && statusCode < 600)) {
ctx.status = statusCode;
} else if (!(ctx.status >= 500 && ctx.status < 600)) {
ctx.status = statusCodes.INTERNAL_SERVER_ERROR;
}
// if (!!statusCode && (statusCode >= 500 && statusCode < 600)) {
// ctx.status = statusCode;
// } else if (!(ctx.status >= 500 && ctx.status < 600)) {
// ctx.status = statusCodes.INTERNAL_SERVER_ERROR;
// }
ctx.body = { status, code, data, message };
};
// ctx.body = { status, code, data, message };
// };
ctx.res.ok = (params = {}) => {
ctx.res.success({
@ -68,20 +68,6 @@ function responseHandler() {
});
};
ctx.res.created = (params = {}) => {
ctx.res.success({
...params,
statusCode: statusCodes.CREATED,
});
};
ctx.res.accepted = (params = {}) => {
ctx.res.success({
...params,
statusCode: statusCodes.ACCEPTED,
});
};
ctx.res.noContent = (params = {}) => {
ctx.res.success({
...params,
@ -89,75 +75,76 @@ function responseHandler() {
});
};
ctx.res.badRequest = (params = {}) => {
ctx.res.fail({
...params,
statusCode: statusCodes.BAD_REQUEST,
});
};
// ctx.res.badRequest = (params = {}) => {
// ctx.res.fail({
// ...params,
// statusCode: statusCodes.BAD_REQUEST,
// });
// };
ctx.res.forbidden = (params = {}) => {
ctx.res.fail({
...params,
statusCode: statusCodes.FORBIDDEN,
});
};
// ctx.res.forbidden = (params = {}) => {
// ctx.res.fail({
// ...params,
// statusCode: statusCodes.FORBIDDEN,
// });
// };
ctx.res.notFound = (params = {}) => {
ctx.res.fail({
...params,
statusCode: statusCodes.NOT_FOUND,
});
};
// ctx.res.notFound = (params = {}) => {
// ctx.res.fail({
// ...params,
// statusCode: statusCodes.NOT_FOUND,
// });
// };
ctx.res.requestTimeout = (params = {}) => {
ctx.res.fail({
...params,
statusCode: statusCodes.REQUEST_TIMEOUT,
});
};
// ctx.res.requestTimeout = (params = {}) => {
// ctx.res.fail({
// ...params,
// statusCode: statusCodes.REQUEST_TIMEOUT,
// });
// };
ctx.res.unprocessableEntity = (params = {}) => {
ctx.res.fail({
...params,
statusCode: statusCodes.UNPROCESSABLE_ENTITY,
});
};
// ctx.res.unprocessableEntity = (params = {}) => {
// ctx.res.fail({
// ...params,
// statusCode: statusCodes.UNPROCESSABLE_ENTITY,
// });
// };
ctx.res.internalServerError = (params = {}) => {
ctx.res.error({
...params,
statusCode: statusCodes.INTERNAL_SERVER_ERROR,
});
};
// ctx.res.internalServerError = (params = {}) => {
// ctx.res.error({
// ...params,
// statusCode: statusCodes.INTERNAL_SERVER_ERROR,
// });
// };
ctx.res.notImplemented = (params = {}) => {
ctx.res.error({
...params,
statusCode: statusCodes.NOT_IMPLEMENTED,
});
};
// ctx.res.notImplemented = (params = {}) => {
// ctx.res.error({
// ...params,
// statusCode: statusCodes.NOT_IMPLEMENTED,
// });
// };
ctx.res.badGateway = (params = {}) => {
ctx.res.error({
...params,
statusCode: statusCodes.BAD_GATEWAY,
});
};
// ctx.res.badGateway = (params = {}) => {
// ctx.res.error({
// ...params,
// statusCode: statusCodes.BAD_GATEWAY,
// });
// };
ctx.res.serviceUnavailable = (params = {}) => {
ctx.res.error({
...params,
statusCode: statusCodes.SERVICE_UNAVAILABLE,
});
};
// ctx.res.serviceUnavailable = (params = {}) => {
// ctx.res.error({
// ...params,
// statusCode: statusCodes.SERVICE_UNAVAILABLE,
// });
// };
// ctx.res.gatewayTimeOut = (params = {}) => {
// ctx.res.error({
// ...params,
// statusCode: statusCodes.GATEWAY_TIME_OUT,
// });
// };
ctx.res.gatewayTimeOut = (params = {}) => {
ctx.res.error({
...params,
statusCode: statusCodes.GATEWAY_TIME_OUT,
});
};
await next();
};
}

View File

@ -1,6 +1,4 @@
const pathToRegExp = require('path-to-regexp');
const readall = require('readall');
const crypto = require('crypto');
const paired = (route, path) => {
const options = {
@ -11,24 +9,6 @@ const paired = (route, path) => {
return pathToRegExp(route, [], options).exec(path);
};
// eslint-disable-next-line no-unused-vars
const read = (stream) =>
new Promise((resolve, reject) => {
readall(stream, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
const md5 = (str) =>
crypto
.createHash('md5')
.update(str)
.digest('hex');
const validityCheck = (routes, exclude, path) => {
let match = false;
let routeExpire = false;
@ -59,7 +39,5 @@ const validityCheck = (routes, exclude, path) => {
module.exports = {
paired,
read,
md5,
validityCheck,
};

View File

@ -9,16 +9,11 @@ module.exports = async (ctx, next) => {
ctx.debug.ips[ip] = 0;
}
ctx.debug.ips[ip]++;
if (ctx.request.path !== '/') {
ctx.debug.request++;
}
ctx.debug.request++;
await next();
if (ctx.request.path !== '/') {
if (ctx.response.get('X-Koa-Redis-Cache') || ctx.response.get('X-Koa-Memory-Cache')) {
ctx.debug.hitCache++;
}
if (ctx.response.get('X-Koa-Redis-Cache') || ctx.response.get('X-Koa-Memory-Cache')) {
ctx.debug.hitCache++;
}
};

View File

@ -2,6 +2,7 @@
const lru = require('lru-cache');
const common = require('./cache-common');
const md5 = require('../utils/md5');
module.exports = function(options = {}) {
const {
@ -55,7 +56,7 @@ module.exports = function(options = {}) {
return async function cache(ctx, next) {
const { url, path } = ctx.request;
const resolvedPrefix = typeof prefix === 'function' ? prefix.call(ctx, ctx) : prefix;
const key = resolvedPrefix + common.md5(ignoreQuery ? path : url);
const key = resolvedPrefix + md5(ignoreQuery ? path : url);
const tkey = key + ':type';
const validityCheck = common.validityCheck(routes, exclude, path);

View File

@ -3,6 +3,7 @@
const wrapper = require('co-redis');
const Redis = require('redis');
const common = require('./cache-common');
const md5 = require('../utils/md5');
module.exports = function(options = {}) {
let redisAvailable = false;
@ -68,7 +69,7 @@ module.exports = function(options = {}) {
return async function cache(ctx, next) {
const { url, path } = ctx.request;
const resolvedPrefix = typeof prefix === 'function' ? prefix.call(ctx, ctx) : prefix;
const key = resolvedPrefix + common.md5(ignoreQuery ? path : url);
const key = resolvedPrefix + md5(ignoreQuery ? path : url);
const tkey = key + ':type';
const validityCheck = common.validityCheck(routes, exclude, path);

View File

@ -87,23 +87,23 @@ router.get('/', async (ctx) => {
});
router.get('/test/:id', (ctx) => {
if (ctx.params.id === '0') {
throw Error('Error test');
}
const item = [];
for (let i = 1; i < 6; i++) {
item.push({
title: `Title${i}`,
description: `Item${i}`,
pubDate: new Date(`2018-4-${i}`).toUTCString(),
link: `https://github.com/DIYgod/RSSHub/issues/${i}`,
author: `DIYgod${i}`,
});
}
ctx.state.data = {
title: `Test ${ctx.params.id}`,
link: 'https://github.com/DIYgod/RSSHub',
item: [
{
title: 'Title1',
description: 'Item1',
pubDate: new Date('2018-4-2').toUTCString(),
link: 'https://github.com/DIYgod/RSSHub/issues/1',
},
{
title: 'Title2',
description: 'Item2',
pubDate: new Date('2018-4-10').toUTCString(),
link: 'https://github.com/DIYgod/RSSHub/issues/2',
},
],
item: item,
};
});
@ -123,6 +123,7 @@ router.get('/bilibili/partion/ranking/:tid/:days?', require('./routes/bilibili/p
router.get('/bilibili/bangumi/:seasonid', require('./routes/bilibili/bangumi')); // 弃用
router.get('/bilibili/bangumi/media/:mediaid', require('./routes/bilibili/bangumi'));
router.get('/bilibili/video/reply/:aid', require('./routes/bilibili/reply'));
router.get('/bilibili/video/danmaku/:aid/:pid?', require('./routes/bilibili/danmaku'));
router.get('/bilibili/link/news/:product', require('./routes/bilibili/linkNews'));
router.get('/bilibili/live/room/:roomID', require('./routes/bilibili/liveRoom'));
router.get('/bilibili/live/search/:key/:order', require('./routes/bilibili/liveSearch'));
@ -226,6 +227,7 @@ router.get('/douban/event/hot/:locationId', require('./routes/douban/event/hot')
router.get('/douban/commercialpress/latest', require('./routes/douban/commercialpress/latest'));
router.get('/douban/bookstore', require('./routes/douban/bookstore'));
router.get('/douban/book/rank/:type', require('./routes/douban/book/rank'));
router.get('/douban/doulist/:id', require('./routes/douban/doulist'));
// 煎蛋
router.get('/jandan/:sub_model', require('./routes/jandan/pic'));
@ -956,6 +958,7 @@ router.get('/zimuzu/resource/:id?', require('./routes/zimuzu/resource'));
// 虎嗅
router.get('/huxiu/tag/:id', require('./routes/huxiu/tag'));
router.get('/huxiu/search/:keyword', require('./routes/huxiu/search'));
router.get('/huxiu/author/:id', require('./routes/huxiu/author'));
// Steam
router.get('/steam/search/:params', require('./routes/steam/search'));
@ -1013,4 +1016,7 @@ router.get('/fitchratings/site/:type', require('./routes/fitchratings/site'));
// 移动支付
router.get('/mpaypass/news', require('./routes/mpaypass/news'));
// 新浪科技探索
router.get('/sina/discovery/:type', require('./routes/sina/discovery'));
module.exports = router;

View File

@ -102,4 +102,24 @@ module.exports = {
}
return name;
},
getCidFromAid: async (ctx, aid, pid) => {
const key = `bili-Cid-from-Aid-${aid}-${pid}`;
let cid = await ctx.cache.get(key);
if (!cid) {
const cidResponse = await axios({
method: 'get',
url: `https://api.bilibili.com/x/web-interface/view?aid=${aid}`,
headers: {
Referer: `https://www.bilibili.com/video/av${aid}`,
},
});
if (cidResponse && cidResponse.data && cidResponse.data.data && cidResponse.data.data.pages && cidResponse.data.data.pages.length >= pid) {
cid = cidResponse.data.data.pages[pid - 1].cid;
}
if (!cid) {
ctx.cache.set(key, cid, 24 * 60 * 60);
}
}
return cid;
},
};

View File

@ -0,0 +1,35 @@
const cheerio = require('cheerio');
const cache = require('./cache');
const fetch = require('node-fetch');
module.exports = async (ctx) => {
const aid = ctx.params.aid;
const pid = Number(ctx.params.pid || 1);
const limit = 50;
const cid = await cache.getCidFromAid(ctx, aid, pid);
const videoName = await cache.getVideoNameFromAid(ctx, aid);
const danmakuResponse = await fetch(`https://comment.bilibili.com/${cid}.xml`);
const danmakuText = await danmakuResponse.text();
let danmakuList = [];
const $ = cheerio.load(danmakuText, { xmlMode: true });
$('d').each((index, item) => {
danmakuList.push({ p: $(item).attr('p'), text: $(item).text() });
});
danmakuList = danmakuList.reverse().slice(0, limit);
ctx.state.data = {
title: `${videoName} 的 弹幕动态`,
link: `https://www.bilibili.com/video/av${aid}`,
description: `${videoName} 的 弹幕动态`,
item: danmakuList.map((item) => ({
title: item.text,
description: item.text,
pubDate: new Date(item.p.split(',')[4] * 1000).toUTCString(),
guid: `${cid}-${item.p.split(',')[4]}-${item.p.split(',')[7]}`,
link: `https://www.bilibili.com/video/av${aid}`,
})),
};
};

View File

@ -0,0 +1,54 @@
const axios = require('../../utils/axios');
const cheerio = require('cheerio');
const url = require('url');
const host = 'https://www.douban.com/doulist/';
module.exports = async (ctx) => {
const id = ctx.params.id;
const link = url.resolve(host, id);
const response = await axios.get(link);
const $ = cheerio.load(response.data);
const title = $('#content h1')
.text()
.trim();
const description = $('div.doulist-about')
.text()
.trim();
const out = $('div.doulist-item')
.slice(0, 10)
.map(function() {
const title = $(this)
.find('div.bd.doulist-note div.title a')
.text()
.trim();
const link = $(this)
.find('div.bd.doulist-note div.title a')
.attr('href');
const date = $(this)
.find('div.ft div.actions time span')
.attr('title');
const description = $(this)
.find('div.bd.doulist-note div.abstract')
.text()
.trim();
const single = {
title: title,
link: link,
description: description,
pubDate: new Date(date).toUTCString(),
};
return single;
})
.get();
ctx.state.data = {
title: title,
link: link,
description: description,
item: out,
};
};

View File

@ -0,0 +1,61 @@
const axios = require('../../utils/axios');
const cheerio = require('cheerio');
const url = require('url');
const date = require('../../utils/date');
module.exports = async (ctx) => {
const { id } = ctx.params;
const link = `https://www.huxiu.com/member/${id}.html`;
const host = 'https://www.huxiu.com';
const response = await axios({
method: 'get',
url: link,
headers: {
Referer: link,
},
});
const $ = cheerio.load(response.data);
const author = $('.user-name')
.text()
.trim();
const list = $('.message-box > .mod-art > a')
.slice(0, 10)
.get()
.map((e) => $(e).attr('href'));
const items = await Promise.all(
list.map(async (e) => {
const itemUrl = url.resolve(host, e);
const single = await ctx.cache.tryGet(
itemUrl,
async () => {
const response = await axios.get(itemUrl);
const $ = cheerio.load(response.data);
$('.neirong-shouquan, .neirong-shouquan-public').remove();
return {
title: $('.t-h1')
.text()
.trim(),
link: itemUrl,
description: $('.article-img-box').html() + $('.article-content-wrap').html(),
pubDate: date($('.article-time').text(), 8),
author,
};
},
2 * 24 * 60 * 60
);
return Promise.resolve(single);
})
);
const authorInfo = `虎嗅网 - ${author}`;
ctx.state.data = {
title: authorInfo,
link,
description: authorInfo,
item: items,
};
};

View File

@ -3,5 +3,5 @@ const { getPage } = require('./util');
module.exports = async (ctx) => {
const { gid } = ctx.params;
ctx.state.data = await getPage(`https://www.javbus.com/genre/${gid}`);
ctx.state.data = await getPage(`https://www.javbus.com/genre/${gid}`, ctx);
};

View File

@ -3,5 +3,5 @@ const { getPage } = require('./util');
module.exports = async (ctx) => {
const { sid } = ctx.params;
ctx.state.data = await getPage(`https://www.javbus.com/star/${sid}`);
ctx.state.data = await getPage(`https://www.javbus.com/star/${sid}`, ctx);
};

View File

@ -2,6 +2,5 @@ const { getPage } = require('../util');
module.exports = async (ctx) => {
const { gid } = ctx.params;
ctx.state.data = await getPage(`https://www.javbus.com/uncensored/genre/${gid}`);
ctx.state.data = await getPage(`https://www.javbus.com/uncensored/genre/${gid}`, ctx);
};

View File

@ -3,5 +3,5 @@ const { getPage } = require('../util');
module.exports = async (ctx) => {
const { sid } = ctx.params;
ctx.state.data = await getPage(`https://www.javbus.com/uncensored/star/${sid}`);
ctx.state.data = await getPage(`https://www.javbus.com/uncensored/star/${sid}`, ctx);
};

View File

@ -1,8 +1,8 @@
const axios = require('../../utils/axios');
const { load } = require('cheerio');
exports.getPage = async (url) => {
const { data } = await axios.get(url);
exports.getPage = async (url, ctx) => {
const { data } = await axios(url);
const $ = load(data);
const pageTitle = `JavBus - ${$('head > title')
@ -13,15 +13,15 @@ exports.getPage = async (url) => {
title: pageTitle,
link: url,
description: pageTitle,
item: parseItems($),
item: await Promise.all(parseItems($, ctx)),
};
};
exports.createHandler = (url) => async (ctx) => {
ctx.state.data = await exports.getPage(url);
ctx.state.data = await exports.getPage(url, ctx);
};
const parseItems = ($) =>
const parseItems = ($, ctx) =>
$('.movie-box')
.map((_, ele) => ({
title: $(ele)
@ -41,15 +41,108 @@ const parseItems = ($) =>
.text(),
}))
.toArray()
.map(render);
const render = ({ title, thumb, link, pubDate, aid }) => ({
title,
link,
pubDate,
description: `
<h1>${aid} - ${title}</h1>
.map(async ({ title, thumb, link, pubDate, aid }) => {
const detail = await getDetail(link, ctx);
title = `${aid} ${title}${detail.filmTime}`;
if (detail.screenData) {
detail.imgHTML = detail.screenData.map((url) => `<img referrerpolicy="no-referrer" src="${url}"><br />`);
}
return {
title,
link,
pubDate,
description: `
<h3>${title}</h3>
<br />
<img referrerpolicy="no-referrer" src="${thumb.replace(/\/thumbs?\//, '/cover/').replace('.jpg', '_b.jpg')}" />
${detail.filmTime ? `<h3>发售日期 : ${detail.filmTime}</h3>` : ''}
${detail.actresses ? `<h3>女优 : ${detail.actresses}</h3>` : ''}
${detail.filmEstabName ? `<h3>發行商 : ${detail.filmEstabName}</h3>` : ''}
${detail.filmMakerbName ? `<h3>製作商 : ${detail.filmMakerbName}</h3>` : ''}
${detail.seriesName ? `<h3>系列 : ${detail.seriesName}</h3>` : ''}
${detail.screenData ? `<h3>影片截图 : </h3>${detail.imgHTML.join('')}` : ''}
`.trim(),
});
};
});
const getDetail = async (link, ctx) => {
const cache = await ctx.cache.get(link);
if (cache) {
return JSON.parse(cache);
} else {
const resp = await axios(link);
const detailPage = resp.data;
// 演员
const actressReg = /<a class="avatar-box"[\s\S]*?<\/a>/g;
let match = detailPage.match(actressReg);
let actresses = [];
if (match) {
match.map((i) => {
const name = /<span>(.*?)<\/span>/.exec(i)[1];
actresses = actresses.concat(name);
return null;
});
} else {
actresses = null; // []
}
// 影片详情
// const filmCover = /<a class="bigImage" href="(.*?)"/.exec(detailPage)[1];
// const filmName = /<a class="bigImage" href="(.*?)" title="(.*?)"/.exec(detailPage)[2];
let filmTime = /<span class="header">發行日期:<\/span>([\s\S]*?)<\/p>/.exec(detailPage);
if (filmTime) {
filmTime = filmTime[1];
} else {
filmTime = null;
}
let filmLast = /<span class="header">長度:<\/span>([\s\S]*?)<\/p>/.exec(detailPage);
if (filmLast) {
const lastmins = /\d+/.exec(filmLast[1]);
const hours = Math.floor(lastmins / 60);
let mins = lastmins % 60;
mins = mins === '0' ? '00' : mins;
filmLast = hours + ':' + mins;
} else {
filmLast = null;
}
let filmEstabName = /<span class="header">發行商:[\s\S]*?"(.*?)">(.*?)<\/a>/.exec(detailPage);
if (filmEstabName) {
filmEstabName = filmEstabName[2];
} else {
filmEstabName = null;
}
let filmMakerbName = /<span class="header">製作商:[\s\S]*?"(.*?)">(.*?)<\/a>/.exec(detailPage);
if (filmMakerbName) {
filmMakerbName = filmMakerbName[2];
} else {
filmMakerbName = null;
}
let seriesName = /<span class="header">系列:[\s\S]*?"(.*?)">(.*?)<\/a>/.exec(detailPage);
if (seriesName) {
seriesName = seriesName[2];
} else {
seriesName = null;
}
let directorName = /<span class="header">導演:[\s\S]*?"(.*?)">(.*?)<\/a>/.exec(detailPage);
if (directorName) {
directorName = directorName[2];
} else {
directorName = null;
}
const code = /<span class="header">識別碼:[\s\S]*?">([\s\S]*?)<\/span>/.exec(detailPage)[1];
// 影片截图
const regScreenshot = /<a class="sample-box" href="(.*?)"[\s\S]*?<img src="(.*?)">/g;
match = detailPage.match(regScreenshot);
let screenData = [];
if (match) {
screenData = match.map((i) => /<a class="sample-box" href="(.*?)"[\s\S]*?<img src="(.*?)">/g.exec(i)[1]);
} else {
screenData = null;
}
const detail = { actresses, filmTime, filmLast, filmEstabName, filmMakerbName, seriesName, directorName, code, screenData };
ctx.cache.set(link, JSON.stringify(detail), 24 * 60 * 60);
return detail;
}
};

View File

@ -3,5 +3,5 @@ const { getPage } = require('../util');
module.exports = async (ctx) => {
const { gid } = ctx.params;
ctx.state.data = await getPage(`https://www.javbus.work/genre/${gid}`);
ctx.state.data = await getPage(`https://www.javbus.work/genre/${gid}`, ctx);
};

View File

@ -3,5 +3,5 @@ const { getPage } = require('../util');
module.exports = async (ctx) => {
const { sid } = ctx.params;
ctx.state.data = await getPage(`https://www.javbus.work/star/${sid}`);
ctx.state.data = await getPage(`https://www.javbus.work/star/${sid}`, ctx);
};

View File

@ -1,5 +1,7 @@
const axios = require('../../utils/axios');
const common = require('./common');
const cheerio = require('cheerio');
const dayjs = require('dayjs');
module.exports = async (ctx) => {
const id = ctx.params.id;
@ -32,4 +34,34 @@ module.exports = async (ctx) => {
image: topic.squarePicture.picUrl || topic.squarePicture.middlePicUrl || topic.squarePicture.thumbnailUrl,
item: common.topicDataHanding(data),
};
if (id === '553870e8e4b0cafb0a1bef68' || id === '55963702e4b0d84d2c30ce6f') {
const promises = ctx.state.data.item.map(async (one) => {
const item = { ...one };
const regResult = /https:\/\/www.okjike.com\/medium\/[a-zA-Z0-9]*/.exec(item.description);
if (regResult) {
const newsUrl = regResult[0];
const cache = ctx.cache.get(newsUrl);
if (cache) {
item.description = cache;
} else {
const { data } = await axios.get(newsUrl);
const $ = cheerio.load(data);
const upper = $('ul.main > li.item');
const links = upper.find('a').map((_, ele) => $(ele).attr('href'));
const texts = upper.find('span.text').map((_, ele) => $(ele).text());
let description = '';
for (let i = 0; i < links.length; i++) {
description += `${i + 1}、<a href="${links[i]}">${texts[i]}</a><br/>`;
}
if (description) {
item.description = description;
ctx.cache.set(newsUrl, description);
}
}
}
item.title = `${topic.content} ${dayjs(new Date(one.pubDate)).format('MM月DD日')}`;
return item;
});
ctx.state.data.item = await Promise.all(promises);
}
};

View File

@ -1,25 +1,32 @@
const axios = require('../../utils/axios');
const dayjs = require('dayjs');
module.exports = async (ctx) => {
const category = ctx.params.category;
let title;
let link;
let path = category;
switch (category) {
case 'topic':
title = '热门话题';
link = 'https://readhub.cn/topics';
break;
case 'news':
title = '科技动态';
link = 'https://readhub.cn/news';
break;
case 'technews':
title = '开发者资讯';
link = 'https://readhub.cn/tech';
break;
case 'blockchain':
title = '区块链快讯';
link = 'https://readhub.cn/blockchain';
break;
case 'daily':
title = '每日早报';
link = 'https://readhub.cn/daily';
break;
default:
break;
@ -29,22 +36,63 @@ module.exports = async (ctx) => {
path = 'topic/daily';
}
const response = await axios({
const {
data: { data },
} = await axios({
method: 'get',
url: `https://api.readhub.cn/${path}`,
});
const data = response.data;
const promises = data.map(async (news) => {
const id = news.id;
if (category === 'topic' || category === 'daily') {
const link = `https://readhub.cn/topic/${id}`;
const cache = ctx.cache.get(link);
if (cache) {
return cache;
}
const { data } = await axios.get(`https://api.readhub.cn/topic/${id}`);
let description = data.summary;
if (data.newsArray) {
description += '<br/><br/>媒体报道:';
for (const one of data.newsArray) {
description += `<br/>${dayjs(new Date(one.publishDate)).format('YY-MM-DD')} ${one.siteName}: <a href='${one.mobileUrl || one.url}'>${one.title}</a>`;
}
}
if (data.timeline && data.timeline.topics) {
let type = '相关事件';
if (data.timeline.commonEntities && data.timeline.commonEntities.length > 0) {
type = '事件追踪';
}
description += `<br/><br/>${type}`;
for (const one of data.timeline.topics) {
description += `<br/>${dayjs(new Date(one.createdAt)).format('YY-MM-DD')} <a href='https://readhub.cn/topic/${one.id}'>${one.title.trim()}</a>`;
}
}
const item = {
title: data.title,
description: description.replace(new RegExp('\n', 'g'), '<br/>'),
pubDate: data.publishDate,
guid: id,
link,
};
ctx.cache.set(link, description);
return item;
} else {
const description = news.summaryAuto || news.summary || news.title;
return {
title: news.title,
description: description.replace(new RegExp('\n', 'g'), '<br/>'),
pubDate: news.publishDate,
guid: id,
link: news.url || news.mobileUrl,
};
}
});
ctx.state.data = {
title: `Readhub-${title}`,
link: 'https://readhub.cn',
item: data.data.map((item) => ({
title: item.title,
pubDate: new Date(item.publishDate || Date.now()).toUTCString(),
description: item.summary || item.title,
guid: `/${category}/${item.id}`,
link: category === 'daily' ? `https://readhub.cn/topic/${item.id}` : item.url || item.newsArray[0].url,
})),
link,
item: await Promise.all(promises),
};
};

View File

@ -0,0 +1,52 @@
const axios = require('../../utils/axios');
const cheerio = require('cheerio');
const link = 'https://tech.sina.com.cn/discovery/';
const api = 'https://feed.sina.com.cn/api/roll/get?pageid=207&num=10&versionNumber=1.2.4&page=1&encode=utf-8&lid=';
const map = new Map([
['zx', { title: '最新', id: '1795' }],
['twhk', { title: '天文航空', id: '1796' }],
['dwzw', { title: '动物植物', id: '1797' }],
['zrdl', { title: '自然地理', id: '1798' }],
['lskg', { title: '历史考古', id: '1799' }],
['smyx', { title: '生命医学', id: '1800' }],
['shbk', { title: '生活百科', id: '1801' }],
['kjqy', { title: '科技前沿', id: '1802' }],
]);
module.exports = async (ctx) => {
const type = ctx.params.type;
const id = map.get(type).id;
const title = map.get(type).title;
const api_url = api + id;
const response = await axios.get(api_url);
const list = response.data.result.data;
const out = await Promise.all(
list.map(async (data) => {
const title = data.title;
const date = data.intime * 1000;
const itemUrl = data.url;
const response = await axios.get(itemUrl);
const $ = cheerio.load(response.data);
const description = $('.article').html();
const single = {
title: title,
link: itemUrl,
description: description,
pubDate: new Date(date).toUTCString(),
};
return Promise.resolve(single);
})
);
ctx.state.data = {
title: `${title}-新浪科技科学探索`,
link: link,
item: out,
};
};

View File

@ -1,6 +1,6 @@
const axios = require('../../../utils/axios');
const cheerio = require('cheerio');
// const timezone = require('../../utils/timezone');
const date = require('../../../utils/date');
module.exports = async (ctx) => {
const { id } = ctx.params;
@ -24,7 +24,7 @@ module.exports = async (ctx) => {
link: `https://wemp.app${$(e)
.find('.post-item__title')
.attr('href')}`,
pubDate: timezone(
pubDate: date(
`${year} ${$(e)
.find('.post-item__date')
.text()
@ -34,11 +34,6 @@ module.exports = async (ctx) => {
author,
}));
// to be replaced
function timezone(html, timezone) {
return new Date(new Date(html).getTime() - 60 * 60 * 1000 * (timezone + new Date().getTimezoneOffset() / 60)).toUTCString();
}
ctx.state.data = {
title: `微信公众号 - ${author}`,
link: `https://wemp.app/accounts/${id}/`,

View File

@ -10,7 +10,8 @@
"docs:build": "vuepress build docs",
"format": "eslint \"**/*.js\" --fix && prettier \"**/*.{js,json,md}\" --write",
"lint": "eslint \"**/*.js\" && prettier-check \"**/*.{js,json,md}\"",
"test": "npm run lint && NODE_ENV=test jest --coverage --runInBand --forceExit"
"test": "npm run lint && NODE_ENV=test jest --coverage --runInBand --forceExit",
"jest": "NODE_ENV=test jest --runInBand --forceExit"
},
"repository": {
"type": "git",
@ -34,7 +35,7 @@
"eslint-config-prettier": "4.0.0",
"eslint-plugin-prettier": "3.0.1",
"jest": "24.0.0",
"lint-staged": "8.1.0",
"lint-staged": "8.1.1",
"mockdate": "2.0.2",
"nodemon": "1.18.9",
"prettier": "1.16.1",
@ -53,7 +54,7 @@
"dayjs": "^1.7.7",
"form-data": "^2.3.2",
"git-rev-sync": "1.12.0",
"googleapis": "36.0.0",
"googleapis": "37.1.0",
"he": "^1.1.1",
"iconv-lite": "0.4.24",
"imgur": "^0.3.1",
@ -66,18 +67,18 @@
"lru-cache": "5.1.1",
"luxon": "^1.7.1",
"markdown-it": "^8.4.2",
"node-fetch": "^2.3.0",
"path-to-regexp": "3.0.0",
"pidusage": "^2.0.17",
"plist": "^3.0.1",
"puppeteer": "^1.10.0",
"raven": "^2.6.4",
"readall": "1.1.0",
"redis": "2.8.0",
"rss-parser": "3.6.2",
"sharp": "^0.21.0",
"socks-proxy-agent": "^4.0.1",
"twit": "2.2.11",
"winston": "3.2.0"
"winston": "3.2.1"
},
"engines": {
"node": ">=8.0.0"

View File

@ -12,26 +12,46 @@ async function checkBlock(response) {
}
afterEach(() => {
process.env.BLACKLIST = undefined;
delete process.env.BLACKLIST;
delete process.env.WHITELIST;
jest.resetModules();
server.close();
});
describe('access-control', () => {
it(`blacklist`, async () => {
process.env.BLACKLIST = '/test/1,/test/2,233.233.233.233';
process.env.BLACKLIST = '/test/1,233.233.233.233';
server = require('../../lib/index').server;
const request = supertest(server);
const response1 = await request.get('/test/1');
checkBlock(response1);
const response11 = await request.get('/test/1');
checkBlock(response11);
const response2 = await request.get('/test/2');
checkBlock(response2);
const response12 = await request.get('/test/1').set('X-Forwarded-For', '233.233.233.233');
checkBlock(response12);
const response31 = await request.get('/test/3');
expect(response31.status).toBe(200);
const response21 = await request.get('/test/2');
expect(response21.status).toBe(200);
const response32 = await request.get('/test/3').set('X-Forwarded-For', '233.233.233.233');
checkBlock(response32);
const response22 = await request.get('/test/2').set('X-Forwarded-For', '233.233.233.233');
checkBlock(response22);
});
it(`whitelist`, async () => {
process.env.WHITELIST = '/test/1,233.233.233.233';
server = require('../../lib/index').server;
const request = supertest(server);
const response11 = await request.get('/test/1');
expect(response11.status).toBe(200);
const response12 = await request.get('/test/1').set('X-Forwarded-For', '233.233.233.233');
expect(response12.status).toBe(200);
const response21 = await request.get('/test/2');
checkBlock(response21);
const response22 = await request.get('/test/2').set('X-Forwarded-For', '233.233.233.233');
expect(response22.status).toBe(200);
});
});

52
test/middleware/debug.js Normal file
View File

@ -0,0 +1,52 @@
const supertest = require('supertest');
const { server } = require('../../lib/index');
const request = supertest(server);
const cheerio = require('cheerio');
let gitHash;
try {
gitHash = require('git-rev-sync').short();
} catch (e) {
gitHash = (process.env.HEROKU_SLUG_COMMIT && process.env.HEROKU_SLUG_COMMIT.slice(0, 7)) || 'unknown';
}
afterAll(() => {
server.close();
});
describe('debug', () => {
it(`debug`, async () => {
await request.get('/test/1').set('X-Forwarded-For', '233.233.233.233');
await request.get('/test/1').set('X-Forwarded-For', '233.233.233.233');
await request.get('/test/1').set('X-Forwarded-For', '233.233.233.234');
await request.get('/test/2').set('X-Forwarded-For', '233.233.233.233');
await request.get('/test/2').set('X-Forwarded-For', '233.233.233.234');
const response = await request.get('/').set('X-Forwarded-For', '233.233.233.233');
const $ = cheerio.load(response.text);
$('.debug-item').each((index, item) => {
const key = $(item)
.find('.debug-key')
.html()
.trim();
const value = $(item)
.find('.debug-value')
.html()
.trim();
switch (key) {
case 'git hash:':
expect(value).toBe(gitHash);
break;
case '请求数:':
expect(value).toBe('6');
break;
case '热门路由:':
expect(value).toBe(`3&nbsp;&nbsp;/test/1<br>2&nbsp;&nbsp;/test/2<br>1&nbsp;&nbsp;/<br>`);
break;
case '热门IP:':
expect(value).toBe(`4&nbsp;&nbsp;233.233.233.233<br>2&nbsp;&nbsp;233.233.233.234<br>`);
break;
}
});
});
});

14
test/middleware/error.js Normal file
View File

@ -0,0 +1,14 @@
const supertest = require('supertest');
const { server } = require('../../lib/index');
const request = supertest(server);
afterAll(() => {
server.close();
});
describe('error', () => {
it(`error`, async () => {
const response = await request.get('/test/0');
expect(response.text).toMatch(/RSSHub 发生了一些意外: <pre>Error: Error test/);
});
});

View File

@ -0,0 +1,99 @@
const supertest = require('supertest');
const { server } = require('../../lib/index');
const request = supertest(server);
const Parser = require('rss-parser');
const parser = new Parser();
afterAll(() => {
server.close();
});
describe('filter', () => {
it(`filter`, async () => {
const response = await request.get('/test/1?filter=Item4|Title5');
const parsed = await parser.parseString(response.text);
expect(parsed.items.length).toBe(2);
expect(parsed.items[0].title).toBe('Title4');
expect(parsed.items[1].title).toBe('Title5');
});
it(`filter_title`, async () => {
const response = await request.get('/test/1?filter_title=Item4|Title5');
const parsed = await parser.parseString(response.text);
expect(parsed.items.length).toBe(1);
expect(parsed.items[0].title).toBe('Title5');
});
it(`filter_description`, async () => {
const response = await request.get('/test/1?filter_description=Item4|Title5');
const parsed = await parser.parseString(response.text);
expect(parsed.items.length).toBe(1);
expect(parsed.items[0].title).toBe('Title4');
});
it(`filter_author`, async () => {
const response = await request.get('/test/1?filter_author=DIYgod4|DIYgod5');
const parsed = await parser.parseString(response.text);
expect(parsed.items.length).toBe(2);
expect(parsed.items[0].title).toBe('Title4');
expect(parsed.items[1].title).toBe('Title5');
});
it(`filterout`, async () => {
const response = await request.get('/test/1?filterout=Item4|Title5');
const parsed = await parser.parseString(response.text);
expect(parsed.items.length).toBe(3);
expect(parsed.items[0].title).toBe('Title1');
expect(parsed.items[1].title).toBe('Title2');
expect(parsed.items[2].title).toBe('Title3');
});
it(`filterout_title`, async () => {
const response = await request.get('/test/1?filterout_title=Item4|Title5');
const parsed = await parser.parseString(response.text);
expect(parsed.items.length).toBe(4);
expect(parsed.items[0].title).toBe('Title1');
expect(parsed.items[1].title).toBe('Title2');
expect(parsed.items[2].title).toBe('Title3');
expect(parsed.items[3].title).toBe('Title4');
});
it(`filterout_description`, async () => {
const response = await request.get('/test/1?filterout_description=Item4|Title5');
const parsed = await parser.parseString(response.text);
expect(parsed.items.length).toBe(4);
expect(parsed.items[0].title).toBe('Title1');
expect(parsed.items[1].title).toBe('Title2');
expect(parsed.items[2].title).toBe('Title3');
expect(parsed.items[3].title).toBe('Title5');
});
it(`filterout_author`, async () => {
const response = await request.get('/test/1?filterout_author=DIYgod4|DIYgod5');
const parsed = await parser.parseString(response.text);
expect(parsed.items.length).toBe(3);
expect(parsed.items[0].title).toBe('Title1');
expect(parsed.items[1].title).toBe('Title2');
expect(parsed.items[2].title).toBe('Title3');
});
});
describe('limit', () => {
it(`limit`, async () => {
const response = await request.get('/test/1?limit=3');
const parsed = await parser.parseString(response.text);
expect(parsed.items.length).toBe(3);
expect(parsed.items[0].title).toBe('Title1');
expect(parsed.items[1].title).toBe('Title2');
expect(parsed.items[2].title).toBe('Title3');
});
});
describe('tgiv', () => {
it(`tgiv`, async () => {
const response = await request.get('/test/1?tgiv=test');
const parsed = await parser.parseString(response.text);
expect(parsed.items[0].link).toBe(`https://t.me/iv?url=https%3A%2F%2Fgithub.com%2FDIYgod%2FRSSHub%2Fissues%2F1&rhash=test`);
expect(parsed.items[1].link).toBe(`https://t.me/iv?url=https%3A%2F%2Fgithub.com%2FDIYgod%2FRSSHub%2Fissues%2F2&rhash=test`);
});
});

1208
yarn.lock

File diff suppressed because it is too large Load Diff