Merge branch 'master' into pr/justjustCC/14123

This commit is contained in:
DIYgod 2024-01-13 19:45:10 +08:00
commit 9dc7e52db9
No known key found for this signature in database
168 changed files with 5342 additions and 1556 deletions

View File

@ -76,6 +76,8 @@ const calculateValue = () => {
},
proxyStrategy: envs.PROXY_STRATEGY || 'all', // all / on_retry
reverseProxyUrl: envs.REVERSE_PROXY_URL,
pacUri: envs.PAC_URI,
pacScript: envs.PAC_SCRIPT,
// auth
authentication: {
name: envs.HTTP_BASIC_AUTH_NAME || 'usernam3',
@ -121,6 +123,7 @@ const calculateValue = () => {
// Route-specific Configurations
bilibili: {
cookies: bilibili_cookies,
dmImgList: envs.BILIBILI_DM_IMG_LIST,
},
bitbucket: {
username: envs.BITBUCKET_USERNAME,

View File

@ -905,7 +905,7 @@ router.get('/4gamers/tag/:tag', lazyloadRouteHandler('./routes/4gamers/tag'));
router.get('/4gamers/topic/:topic', lazyloadRouteHandler('./routes/4gamers/topic'));
// 大麦网
router.get('/damai/activity/:city/:category/:subcategory/:keyword?', lazyloadRouteHandler('./routes/damai/activity'));
// router.get('/damai/activity/:city/:category/:subcategory/:keyword?', lazyloadRouteHandler('./routes/damai/activity'));
// 桂林电子科技大学新闻资讯
router.get('/guet/xwzx/:type?', lazyloadRouteHandler('./routes/guet/news'));

View File

@ -1,28 +0,0 @@
const got = require('@/utils/got');
module.exports = async (ctx) => {
const city = ctx.params.city === '全部' ? '' : ctx.params.city;
const category = ctx.params.category === '全部' ? '' : ctx.params.category;
const subcategory = ctx.params.subcategory === '全部' ? '' : ctx.params.subcategory;
const keyword = ctx.params.keyword ? ctx.params.keyword : '';
const url = `https://search.damai.cn/searchajax.html?keyword=${encodeURIComponent(keyword)}&cty=${encodeURIComponent(city)}&ctl=${encodeURIComponent(category)}&sctl=${encodeURIComponent(
subcategory
)}&tsg=0&st=&et=&order=3&pageSize=30&currPage=1&tn=`;
const response = await got.get(url);
const data = response.data;
const list = data.pageData.resultData;
ctx.state.data = {
title: `大麦网票务 - ${city ? city : '全国'} - ${category ? category : '全部分类'}${subcategory ? ' - ' + subcategory : ''}${keyword ? ' - ' + keyword : ''}`,
link: 'https://search.damai.cn/search.htm',
item: list.map((item) => ({
title: item.nameNoHtml,
author: item.actors ? item.actors.replace(/<[^<>]*>/, '') : '大麦网',
description: `<img src="${item.verticalPic}" /><p>${item.description}</p><p>地点:${item.venuecity} | ${item.venue}</p><p>时间:${item.showtime}</p><p>票价:${item.price_str}</p>`,
pubDate: new Date(),
link: `https://detail.damai.cn/item.htm?id=${item.projectid}`,
})),
};
};

75
lib/utils/pac-proxy.js Normal file
View File

@ -0,0 +1,75 @@
const config = require('@/config').value;
const logger = require('./logger');
const possibleProtocol = ['http', 'https', 'ftp', 'file', 'data'];
const pacProxy = (pacUri, pacScript, proxyObj) => {
let pacUrlHandler = null;
// Validate PAC_URI / PAC_SCRIPT
if (pacScript) {
if (typeof pacScript === 'string') {
pacUri = 'data:text/javascript;charset=utf-8,' + encodeURIComponent(pacScript);
} else {
logger.error('Invalid PAC_SCRIPT, use PAC_URI instead');
}
}
if (pacUri && typeof pacUri === 'string') {
try {
pacUrlHandler = new URL(pacUri);
} catch (e) {
pacUri = null;
pacUrlHandler = null;
logger.error(`Parse PAC_URI error: ${e.stack}`);
}
} else {
pacUri = null;
}
// Check if PAC_URI has the right protocol
if (pacUri && !possibleProtocol.includes(pacUrlHandler?.protocol?.replace(':', ''))) {
logger.error(`Unsupported PAC protocol: ${pacUrlHandler?.protocol?.replace(':', '')}, expect one of ${possibleProtocol.join(', ')}`);
pacUri = null;
pacUrlHandler = null;
}
// Validate proxyObj
if (pacUrlHandler) {
proxyObj.host = pacUrlHandler.hostname;
proxyObj.port = parseInt(pacUrlHandler.port) || undefined;
proxyObj.protocol = pacUrlHandler.protocol.replace(':', '');
} else {
proxyObj.protocol = proxyObj.host = proxyObj.port = proxyObj.auth = undefined;
}
// Validate PROXY_AUTH
if (proxyObj.auth && pacUrlHandler) {
let promptProxyUri = false;
if (pacUrlHandler.username || pacUrlHandler.password) {
logger.warn('PAC_URI contains username and/or password, ignoring PROXY_AUTH');
proxyObj.auth = undefined;
} else if (!['http:', 'https:'].includes(pacUrlHandler.protocol)) {
logger.warn(`PROXY_AUTH is only supported by HTTP(S) proxies, but got ${pacUrlHandler.protocol}, ignoring`);
proxyObj.auth = undefined;
promptProxyUri = true;
} else {
logger.info('PROXY_AUTH is set and will be used for requests from Node.js. However, requests from puppeteer will not use it');
promptProxyUri = true;
}
if (promptProxyUri) {
logger.info('To get rid of this, set PAC_URI like protocol://username:password@host:port and clear PROXY_{AUTH,PROTOCOL,HOST,PORT}');
}
}
// Compatible with unify-proxy
return {
proxyUri: pacUri,
proxyObj,
proxyUrlHandler: pacUrlHandler,
};
};
module.exports = {
pacProxy,
...pacProxy(config.pacUri, config.pacScript, config.proxy),
};

View File

@ -1,11 +1,15 @@
const config = require('@/config').value;
const { proxyUri, proxyObj, proxyUrlHandler } = require('./unify-proxy');
const proxyIsPAC = config.pacUri || config.pacScript;
const { proxyUri, proxyObj, proxyUrlHandler } = proxyIsPAC ? require('./pac-proxy') : require('./unify-proxy');
const logger = require('./logger');
const http = require('http');
const https = require('https');
let agent = null;
if (proxyUri) {
if (proxyIsPAC) {
const { PacProxyAgent } = require('pac-proxy-agent');
agent = new PacProxyAgent(`pac+${proxyUri}`);
} else if (proxyUri) {
if (proxyUri.startsWith('http')) {
const { HttpsProxyAgent } = require('https-proxy-agent');
agent = new HttpsProxyAgent(proxyUri);

53
lib/v2/1lou/index.js Normal file
View File

@ -0,0 +1,53 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const timezone = require('@/utils/timezone');
const { parseDate } = require('@/utils/parse-date');
module.exports = async (ctx) => {
const path = ctx.params.path ?? '';
const rootUrl = `https://www.1lou.me`;
const currentUrl = `${rootUrl}/${path}`;
const response = await got({
method: 'get',
url: currentUrl,
});
const $ = cheerio.load(response.data);
let items = $('li.media.thread.tap:not(.hidden-sm)')
.toArray()
.map((item) => {
const title = $(item).find('.subject.break-all').children('a').first();
const author = $(item).find('.username.text-grey.mr-1').text();
const pubDate = $(item).find('.date.text-grey').text();
return {
title: title.text(),
link: `${rootUrl}/${title.attr('href')}`,
author,
pubDate: timezone(parseDate(pubDate), +8),
};
});
items = await Promise.all(
items.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const detailResponse = await got({
method: 'get',
url: item.link,
});
const content = cheerio.load(detailResponse.data);
item.description = content('.message.break-all').html();
const torrents = content('.attachlist').find('a');
if (torrents.length > 0) {
item.enclosure_type = 'application/x-bittorrent';
item.enclosure_url = `${rootUrl}/${torrents.first().attr('href')}`;
}
return item;
})
)
);
ctx.state.data = {
title: '1Lou',
link: currentUrl,
item: items,
};
};

View File

@ -0,0 +1,3 @@
module.exports = {
'/:path?': ['falling'],
};

13
lib/v2/1lou/radar.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
'1lou.me': {
_name: 'BT之家 1LOU站',
'.': [
{
title: '搜索',
docs: 'https://docs.rsshub.app/routes/multimedia#bt-zhi-jia-1lou-zhan',
source: ['/:path'],
target: '/1lou/:path',
},
],
},
};

3
lib/v2/1lou/router.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = function (router) {
router.get('/:path?', require('./index'));
};

57
lib/v2/acpaa/index.js Normal file
View File

@ -0,0 +1,57 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const timezone = require('@/utils/timezone');
const { parseDate } = require('@/utils/parse-date');
module.exports = async (ctx) => {
const { id = '1', name = '重要通知' } = ctx.params;
const limit = ctx.query.limit ? parseInt(ctx.query.limit, 10) : 30;
const rootUrl = 'http://www.acpaa.cn';
const currentUrl = new URL(`article/taglist.jhtml?tagIds=${id}&tagname=${name}`, rootUrl).href;
const { data: response } = await got(currentUrl);
const $ = cheerio.load(response);
let items = $('div.text01 ul li a[title]')
.slice(0, limit)
.toArray()
.map((item) => {
item = $(item);
return {
title: item.prop('title'),
link: new URL(item.prop('href'), rootUrl).href,
pubDate: timezone(parseDate(item.find('span[title]').prop('title')), +8),
};
});
items = await Promise.all(
items.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const { data: detailResponse } = await got(item.link);
const content = cheerio.load(detailResponse);
item.title = content('div.xhjj_head01').text();
item.description = content('div.text01').html();
return item;
})
)
);
const author = $('title').text().replace(/-/g, '');
const subtitle = $('span.myTitle').text().trim();
ctx.state.data = {
item: items,
title: `${author} - ${subtitle}`,
link: currentUrl,
description: $('meta[property="og:description"]').prop('content'),
language: 'zh',
subtitle,
author,
};
};

View File

@ -0,0 +1,3 @@
module.exports = {
'/:id?/:name?': ['nczitzk'],
};

19
lib/v2/acpaa/radar.js Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
'acpaa.cn': {
_name: '中华全国专利代理师协会',
'.': [
{
title: '文章',
docs: 'https://docs.rsshub.app/routes/other#zhong-hua-quan-guo-zhuan-li-dai-li-shi-xie-hui',
source: ['/article/taglist.jhtml'],
target: (url) => {
url = new URL(url);
const id = url.searchParams.get('id');
const name = url.searchParams.get('name');
return `/acpaa${id ? `/${id}${name ? `/${name}` : ''}` : ''}`;
},
},
],
},
};

3
lib/v2/acpaa/router.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = (router) => {
router.get('/:id?/:name?', require('./'));
};

44
lib/v2/backlinko/blog.js Normal file
View File

@ -0,0 +1,44 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
module.exports = async (ctx) => {
const baseUrl = 'https://backlinko.com';
const { data: response, url: link } = await got(`${baseUrl}/blog`);
const $ = cheerio.load(response);
const nextData = JSON.parse($('#__NEXT_DATA__').text());
const {
buildId,
props: { pageProps },
} = nextData;
const posts = pageProps.posts.nodes.concat(pageProps.backlinkoLockedPosts.nodes).map((post) => ({
title: post.title,
link: `${baseUrl}/${post.slug}`,
pubDate: parseDate(post.modified),
author: post.author.node.name,
apiUrl: `${baseUrl}/_next/data/${buildId}/${post.slug}.json`,
}));
const items = await Promise.all(
posts.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const { data } = await got(item.apiUrl);
const post = data.pageProps.post || data.pageProps.lockedPost;
item.description = post.content;
return item;
})
)
);
ctx.state.data = {
title: pageProps.page.seo.title,
description: pageProps.page.seo.metaDesc,
link,
language: 'en',
item: items,
};
};

View File

@ -0,0 +1,3 @@
module.exports = {
'/blog': ['TonyRL'],
};

13
lib/v2/backlinko/radar.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
'backlinko.com': {
_name: 'Backlinko',
'.': [
{
title: 'Blog',
docs: 'https://docs.rsshub.app/routes/blog#backlinko',
source: ['/blog', '/'],
target: '/backlinko/blog',
},
],
},
};

View File

@ -0,0 +1,3 @@
module.exports = (router) => {
router.get('/blog', require('./blog'));
};

View File

@ -55,8 +55,8 @@ module.exports = {
return cookie.join('; ');
});
},
getVerifyString: (ctx) => {
const key = 'bili-verify-string';
getWbiVerifyString: (ctx) => {
const key = 'bili-wbi-verify-string';
return ctx.cache.tryGet(key, async () => {
const cookie = await module.exports.getCookie(ctx);
const { data: navResponse } = await got('https://api.bilibili.com/x/web-interface/nav', {
@ -97,14 +97,14 @@ module.exports = {
const key = 'bili-username-from-uid-' + uid;
return ctx.cache.tryGet(key, async () => {
const cookie = await module.exports.getCookie(ctx);
const verifyString = await module.exports.getVerifyString(ctx);
const wbiVerifyString = await module.exports.getWbiVerifyString(ctx);
// await got(`https://space.bilibili.com/${uid}/`, {
// headers: {
// Referer: 'https://www.bilibili.com/',
// Cookie: cookie,
// },
// });
const params = utils.addVerifyInfo(`mid=${uid}&token=&platform=web&web_location=1550101`, verifyString);
const params = utils.addWbiVerifyInfo(`mid=${uid}&token=&platform=web&web_location=1550101`, wbiVerifyString);
const { data: nameResponse } = await got(`https://api.bilibili.com/x/space/wbi/acc/info?${params}`, {
headers: {
Referer: `https://space.bilibili.com/${uid}/`,
@ -121,14 +121,14 @@ module.exports = {
let face = await ctx.cache.get(faceKey);
if (!name || !face) {
const cookie = await module.exports.getCookie(ctx);
const verifyString = await module.exports.getVerifyString(ctx);
const wbiVerifyString = await module.exports.getWbiVerifyString(ctx);
// await got(`https://space.bilibili.com/${uid}/`, {
// headers: {
// Referer: `https://www.bilibili.com/`,
// Cookie: cookie,
// },
// });
const params = utils.addVerifyInfo(`mid=${uid}&token=&platform=web&web_location=1550101`, verifyString);
const params = utils.addWbiVerifyInfo(`mid=${uid}&token=&platform=web&web_location=1550101`, wbiVerifyString);
const { data: nameResponse } = await got(`https://api.bilibili.com/x/space/wbi/acc/info?${params}`, {
headers: {
Referer: `https://space.bilibili.com/${uid}/`,

View File

@ -3,8 +3,8 @@ const cache = require('./cache');
const utils = require('./utils');
module.exports = async (ctx) => {
const verifyString = await cache.getVerifyString(ctx);
const params = utils.addVerifyInfo('limit=10&platform=web', verifyString);
const wbiVerifyString = await cache.getWbiVerifyString(ctx);
const params = utils.addWbiVerifyInfo('limit=10&platform=web', wbiVerifyString);
const url = `https://api.bilibili.com/x/web-interface/wbi/search/square?${params}`;
const response = await got({
method: 'get',

View File

@ -17,9 +17,9 @@ module.exports = async (ctx) => {
orderTitle = '人气直播';
break;
}
const verifyString = await cache.getVerifyString(ctx);
const wbiVerifyString = await cache.getWbiVerifyString(ctx);
let params = `__refresh__=true&_extra=&context=&page=1&page_size=42&order=${order}&duration=&from_source=&from_spmid=333.337&platform=pc&highlight=1&single_column=0&keyword=${urlEncodedKey}&ad_resource=&source_tag=3&gaia_vtoken=&category_id=&search_type=live&dynamic_offset=0&web_location=1430654`;
params = utils.addVerifyInfo(params, verifyString);
params = utils.addWbiVerifyInfo(params, wbiVerifyString);
const response = await got({
method: 'get',

View File

@ -32,6 +32,7 @@ module.exports = (router) => {
router.get('/user/followers/:uid/:loginUid', require('./followers'));
router.get('/user/followings/:uid/:loginUid', require('./followings'));
router.get('/user/video/:uid/:disableEmbed?', require('./video'));
router.get('/user/video-all/:uid/:disableEmbed?', require('./video-all'));
router.get('/video/danmaku/:bvid/:pid?', require('./danmaku'));
router.get('/video/page/:bvid/:disableEmbed?', require('./page'));
router.get('/video/reply/:bvid', require('./reply'));

View File

@ -1,3 +1,4 @@
const config = require('@/config').value;
const md5 = require('@/utils/md5');
const CryptoJS = require('crypto-js');
@ -65,20 +66,58 @@ function hexsign(e) {
return o;
}
function addVerifyInfo(params, verifyString) {
function addWbiVerifyInfo(params, wbiVerifyString) {
const searchParams = new URLSearchParams(params);
searchParams.sort();
const verifyParam = searchParams.toString();
const wts = Math.round(Date.now() / 1000);
const w_rid = md5(`${verifyParam}&wts=${wts}${verifyString}`);
const w_rid = md5(`${verifyParam}&wts=${wts}${wbiVerifyString}`);
return `${params}&w_rid=${w_rid}&wts=${wts}`;
}
// https://github.com/errcw/gaussian/blob/master/lib/box-muller.js
function generateGaussianInteger(mean, std) {
const _2PI = Math.PI * 2;
const u1 = Math.random();
const u2 = Math.random();
const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(_2PI * u2);
return Math.round(z0 * std + mean);
}
function getDmImgList() {
if (typeof config.bilibili.dmImgList !== 'undefined') {
const dmImgList = JSON.parse(config.bilibili.dmImgList);
return JSON.stringify([dmImgList[Math.floor(Math.random() * dmImgList.length)]]);
}
const x = Math.max(generateGaussianInteger(650, 5), 0);
const y = Math.max(generateGaussianInteger(400, 5), 0);
const path = [
{
x: 3 * x + 2 * y,
y: 4 * x - 5 * y,
z: 0,
timestamp: Math.max(generateGaussianInteger(30, 5), 0),
type: 0,
},
];
return JSON.stringify(path);
}
function addDmVerifyInfo(params, dmImgList) {
const dmImgStr = Buffer.from('no webgl').toString('base64').slice(0, -2);
const dmCoverImgStr = Buffer.from('no webgl').toString('base64').slice(0, -2);
return `${params}&dm_img_list=${dmImgList}&dm_img_str=${dmImgStr}&dm_cover_img_str=${dmCoverImgStr}`;
}
module.exports = {
iframe,
lsid,
_uuid,
hexsign,
addVerifyInfo,
addWbiVerifyInfo,
getDmImgList,
addDmVerifyInfo,
bvidTime: 1589990400,
};

View File

@ -0,0 +1,75 @@
const got = require('@/utils/got');
const cache = require('./cache');
const utils = require('./utils');
const { parseDate } = require('@/utils/parse-date');
module.exports = async (ctx) => {
const uid = ctx.params.uid;
const disableEmbed = ctx.params.disableEmbed;
const cookie = await cache.getCookie(ctx);
const wbiVerifyString = await cache.getWbiVerifyString(ctx);
const dmImgList = utils.getDmImgList();
const [name, face] = await cache.getUsernameAndFaceFromUID(ctx, uid);
await got(`https://space.bilibili.com/${uid}/video?tid=0&page=1&keyword=&order=pubdate`, {
headers: {
Referer: `https://space.bilibili.com/${uid}/`,
Cookie: cookie,
},
});
const params = utils.addWbiVerifyInfo(utils.addDmVerifyInfo(`mid=${uid}&ps=30&tid=0&pn=1&keyword=&order=pubdate&platform=web&web_location=1550101&order_avoided=true`, dmImgList), wbiVerifyString);
const response = await got(`https://api.bilibili.com/x/space/wbi/arc/search?${params}`, {
headers: {
Referer: `https://space.bilibili.com/${uid}/video?tid=0&page=1&keyword=&order=pubdate`,
Cookie: cookie,
},
});
const vlist = [...response.data.data.list.vlist];
const pageTotal = Math.ceil(response.data.data.page.count / response.data.data.page.ps);
const getPage = async (pageId) => {
const cookie = await cache.getCookie(ctx);
await got(`https://space.bilibili.com/${uid}/video?tid=0&page=${pageId}&keyword=&order=pubdate`, {
headers: {
Referer: `https://space.bilibili.com/${uid}/`,
Cookie: cookie,
},
});
const params = utils.addWbiVerifyInfo(utils.addDmVerifyInfo(`mid=${uid}&ps=30&tid=0&pn=${pageId}&keyword=&order=pubdate&platform=web&web_location=1550101&order_avoided=true`, dmImgList), wbiVerifyString);
return got(`https://api.bilibili.com/x/space/wbi/arc/search?${params}`, {
headers: {
Referer: `https://space.bilibili.com/${uid}/video?tid=0&page=${pageId}&keyword=&order=pubdate`,
Cookie: cookie,
},
});
};
const promises = [];
if (pageTotal > 1) {
for (let i = 2; i <= pageTotal; i++) {
promises.push(getPage(i));
}
const rets = await Promise.all(promises);
rets.forEach((ret) => {
vlist.push(...ret.data.data.list.vlist);
});
}
ctx.state.data = {
title: name,
link: `https://space.bilibili.com/${uid}/video`,
description: `${name} 的 bilibili 所有视频`,
logo: face,
icon: face,
item: vlist.map((item) => ({
title: item.title,
description: `${item.description}${!disableEmbed ? `<br><br>${utils.iframe(item.aid)}` : ''}<br><img src="${item.pic}">`,
pubDate: parseDate(item.created, 'X'),
link: item.created > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`,
author: name,
comments: item.comment,
})),
};
};

View File

@ -7,7 +7,8 @@ module.exports = async (ctx) => {
const uid = ctx.params.uid;
const disableEmbed = ctx.params.disableEmbed;
const cookie = await cache.getCookie(ctx);
const verifyString = await cache.getVerifyString(ctx);
const wbiVerifyString = await cache.getWbiVerifyString(ctx);
const dmImgList = utils.getDmImgList();
const [name, face] = await cache.getUsernameAndFaceFromUID(ctx, uid);
// await got(`https://space.bilibili.com/${uid}/video?tid=0&page=1&keyword=&order=pubdate`, {
@ -16,14 +17,7 @@ module.exports = async (ctx) => {
// Cookie: cookie,
// },
// });
const params = utils.addVerifyInfo(
`mid=${uid}&ps=30&tid=0&pn=1&keyword=&order=pubdate&platform=web&web_location=1550101&order_avoided=true&dm_img_list=[]&dm_img_str=${Buffer.from('no webgl').toString('base64').slice(0, -2)}&dm_cover_img_str=${Buffer.from(
'no webgl'
)
.toString('base64')
.slice(0, -2)}`,
verifyString
);
const params = utils.addWbiVerifyInfo(utils.addDmVerifyInfo(`mid=${uid}&ps=30&tid=0&pn=1&keyword=&order=pubdate&platform=web&web_location=1550101&order_avoided=true`, dmImgList), wbiVerifyString);
const response = await got(`https://api.bilibili.com/x/space/wbi/arc/search?${params}`, {
headers: {
Referer: `https://space.bilibili.com/${uid}/video?tid=0&page=1&keyword=&order=pubdate`,

View File

@ -1,3 +1,4 @@
module.exports = {
'/keyword/:keyword': ['untitaker'],
'/profile/:handle': ['TonyRL'],
};

42
lib/v2/bsky/posts.js Normal file
View File

@ -0,0 +1,42 @@
const { parseDate } = require('@/utils/parse-date');
const { resolveHandle, getProfile, getAuthorFeed } = require('./utils');
const { art } = require('@/utils/render');
const { join } = require('path');
module.exports = async (ctx) => {
const { handle } = ctx.params;
const DID = await resolveHandle(handle, ctx.cache.tryGet);
const profile = await getProfile(DID, ctx.cache.tryGet);
const authorFeed = await getAuthorFeed(DID, ctx.cache.tryGet);
const items = authorFeed.feed.map(({ post }) => ({
title: post.record.text.split('\n')[0],
description: art(join(__dirname, 'templates/post.art'), {
text: post.record.text.replace(/\n/g, '<br>'),
embed: post.embed,
// embed.$type "app.bsky.embed.record#view" and "app.bsky.embed.recordWithMedia#view"
// are not handled
}),
author: post.author.displayName,
pubDate: parseDate(post.record.createdAt),
link: `https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('app.bsky.feed.post/')[1]}`,
upvotes: post.likeCount,
comments: post.replyCount,
}));
ctx.state.data = {
title: `${profile.displayName} (@${profile.handle}) — Bluesky`,
description: profile.description.replace(/\n/g, ' '),
link: `https://bsky.app/profile/${profile.handle}`,
image: profile.banner,
icon: profile.avatar,
logo: profile.avatar,
item: items,
};
ctx.state.json = {
DID,
profile,
authorFeed,
};
};

View File

@ -8,6 +8,12 @@ module.exports = {
source: '/search',
target: (params, url) => `/bsky/keyword/${new URL(url).searchParams.get('q')}`,
},
{
title: 'Post',
docs: 'https://docs.rsshub.app/routes/social-media#bluesky-bsky',
source: '/profile/:handle',
target: '/bsky/profile/:handle',
},
],
},
};

View File

@ -1,3 +1,4 @@
module.exports = (router) => {
router.get('/keyword/:keyword', require('./keyword'));
router.get('/profile/:handle', require('./posts'));
};

View File

@ -0,0 +1,15 @@
{{ if text }}
{{@ text }}<br>
{{ /if }}
{{ if embed }}
{{ if embed.$type == 'app.bsky.embed.images#view'}}
{{ each embed.images i }}
<img src="{{ i.fullsize }}" alt="{{ i.alt }}"><br>
{{ /each }}
{{ else if embed.$type == 'app.bsky.embed.external#view' }}
<a href="{{ embed.external.uri }}"><b>{{ embed.external.title }}</b><br>
{{ embed.external.description }}
</a>
{{ /if }}
{{ /if }}

52
lib/v2/bsky/utils.js Normal file
View File

@ -0,0 +1,52 @@
const got = require('@/utils/got');
const config = require('@/config').value;
/**
* docs: https://atproto.com/lexicons/app-bsky
*/
// https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/identity/resolveHandle.json
const resolveHandle = (handle, tryGet) =>
tryGet(`bsky:${handle}`, async () => {
const { data } = await got('https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle', {
searchParams: {
handle,
},
});
return data.did;
});
// https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/actor/getProfile.json
const getProfile = (did, tryGet) =>
tryGet(`bsky:profile:${did}`, async () => {
const { data } = await got('https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile', {
searchParams: {
actor: did,
},
});
return data;
});
// https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getAuthorFeed.json
const getAuthorFeed = (did, tryGet) =>
tryGet(
`bsky:authorFeed:${did}`,
async () => {
const { data } = await got('https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed', {
searchParams: {
actor: did,
filter: 'posts_and_author_threads',
limit: 30,
},
});
return data;
},
config.cache.routeExpire,
false
);
module.exports = {
resolveHandle,
getProfile,
getAuthorFeed,
};

View File

@ -100,7 +100,8 @@ const processItems = async (apiUrl, limit, tryGet, ...searchParams) => {
});
item.title = data.title;
item.link = /^\/\//.test(data.link_url) ? `https:${data.link_url}` : data.link_url;
// api 返回的 link_url 数据可能是空, 所有最好自己通过 id 拼接 url
item.link = `${rootUrl}/article/${item.guid}.html`;
item.description = content.html();
item.author = data.author_name ?? data.author;
item.category = [...new Set(categories)].filter((c) => c);

44
lib/v2/damai/activity.js Normal file
View File

@ -0,0 +1,44 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { art } = require('@/utils/render');
const { join } = require('path');
module.exports = async (ctx) => {
const city = ctx.params.city === '全部' ? '' : ctx.params.city;
const category = ctx.params.category === '全部' ? '' : ctx.params.category;
const subcategory = ctx.params.subcategory === '全部' ? '' : ctx.params.subcategory;
const keyword = ctx.params.keyword ? ctx.params.keyword : '';
const url = 'https://search.damai.cn/searchajax.html';
const response = await got(url, {
searchParams: {
keyword,
cty: city,
ctl: category,
sctl: subcategory,
tsg: 0,
st: '',
et: '',
order: 3,
pageSize: 30,
currPage: 1,
tn: '',
},
});
const data = response.data;
const list = data.pageData.resultData || [];
ctx.state.data = {
title: `大麦网票务 - ${city || '全国'} - ${category || '全部分类'}${subcategory ? ' - ' + subcategory : ''}${keyword ? ' - ' + keyword : ''}`,
link: 'https://search.damai.cn/search.htm',
item: list.map((item) => ({
title: item.nameNoHtml,
author: item.actors ? cheerio.load(item.actors, null, false).text() : '大麦网',
description: art(join(__dirname, 'templates/activity.art'), {
item,
}),
link: `https://detail.damai.cn/item.htm?id=${item.projectid}`,
})),
};
};

View File

@ -0,0 +1,3 @@
module.exports = {
'/activity/:city/:category/:subcategory/:keyword?': ['hoilc'],
};

13
lib/v2/damai/radar.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
'damai.cn': {
_name: '大麦网',
search: [
{
title: '票务更新',
docs: 'https://docs.rsshub.app/routes/shopping#da-mai-wang',
source: ['/search.html'],
target: (_params, url) => `/damai/activity/全部/全部/全部/${new URL(url).searchParams.get('keyword') || ''}`,
},
],
},
};

3
lib/v2/damai/router.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = (router) => {
router.get('/activity/:city/:category/:subcategory/:keyword?', require('./activity'));
};

View File

@ -0,0 +1,5 @@
<img src="{{ item.verticalPic }}">
<p>{{@ item.description }}</p>
<p>地点:{{ item.venuecity }} | {{ item.venue }}</p>
<p>时间:{{ item.showtime }}</p>
<p>票价:{{ item.price_str }}</p>

View File

@ -25,7 +25,7 @@ function getDomList($, detailUrl) {
return list;
}
function getItemList($, detailUrl) {
function getItemList($, detailUrl, second) {
const encoded = $('.article script[type]')
.text()
.match(/return p}\('(.*)',(\d+),(\d+),'(.*)'.split\(/);
@ -39,7 +39,9 @@ function getItemList($, detailUrl) {
.replace(/\\\\"/g, '"')
.replace(/\\\\\\/g, '')
);
const { downurls } = data.Data[0];
// support secondary download address
const { downurls } = second && data.Data.length > 1 ? data.Data[1] : data.Data[0];
return downurls.map((item) => {
const [title, downurl] = item.split('$');
const urlType = getUrlType(downurl);
@ -69,7 +71,7 @@ function getMetaInfo($) {
module.exports = async (ctx) => {
const { id } = ctx.params;
const { domain } = ctx.query;
const { domain, second } = ctx.query;
let pureId = id;
let detailType = 'html';
// compatible for .html suffix in radar
@ -84,7 +86,7 @@ module.exports = async (ctx) => {
const res = await got(detailUrl);
const $ = cheerio.load(res.data);
const list = getItemList($, detailUrl);
const list = getItemList($, detailUrl, second);
const meta = getMetaInfo($);
ctx.state.data = {

View File

@ -2,7 +2,7 @@ const config = require('@/config').value;
const defaultDomain = 'mp4us.com';
const allowedDomains = ['domp4.cc', 'mp4kan.com', 'mp4us.com', 'wemp4.com', 'dbmp4.com'];
const allowedDomains = ['domp4.cc', 'mp4us.com', 'wemp4.com', 'dbmp4.com'];
/**
* trackers from https://www.domp4.cc/Style/2020/js/base.js?v=2

View File

@ -18,7 +18,9 @@ module.exports = async (ctx) => {
const date = new Date();
const year = date.getFullYear();
const mon = date.getMonth() + 1;
const month = date.getMonth() + 1;
const mon = month < 10 ? '0' + month : month.toString();
let items = response.data.data[0].items;
const subjectCollectionId = items.find((item) => item.title.startsWith(`${year}${mon}`)).id;

64
lib/v2/ekantipur/issue.js Normal file
View File

@ -0,0 +1,64 @@
// Require necessary modules
const got = require('@/utils/got'); // a customised got
const cheerio = require('cheerio'); // an HTML parser with a jQuery-like API
module.exports = async (ctx) => {
// Your logic here
// Defining base URL
const baseUrl = 'https://ekantipur.com';
// Retrive the channel parameter
const { channel = 'news' } = ctx.params;
// Fetches content of the requested channel
const { data: response } = await got(`${baseUrl}/${channel}`);
const $ = cheerio.load(response);
// Retrive articles
const list = $('article.normal')
// We use the `toArray()` method to retrieve all the DOM elements selected as an array.
.toArray()
// We use the `map()` method to traverse the array and parse the data we need from each element.
.map((item) => {
item = $(item);
const a = item.find('a').first();
return {
title: a.text(),
// We need an absolute URL for `link`, but `a.attr('href')` returns a relative URL.
link: `${baseUrl}${a.attr('href')}`,
author: item.find('div.author').text(),
category: channel,
};
});
const items = await Promise.all(
list.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const { data: response } = await got(item.link);
const $ = cheerio.load(response);
// Remove sponsor elements
$('a.static-sponsor').remove();
$('div.ekans-wrapper').remove();
// Fetch title from the article page
item.title = $('h1.eng-text-heading').text();
// Fetch article content from the article page
item.description = $('div.current-news-block').first().html();
// Every property of a list item defined above is reused here
// and we add a new property 'description'
return item;
})
)
);
ctx.state.data = {
// channel title
title: `Ekantipur - ${channel}`,
// channel link
link: `${baseUrl}/${channel}`,
// each feed item
item: items,
};
};

View File

@ -0,0 +1,3 @@
module.exports = {
'/:channel?': ['maniche04'],
};

13
lib/v2/ekantipur/radar.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
'ekantipur.com': {
_name: 'Ekantipur',
'.': [
{
title: 'Full Article RSS',
docs: 'https://docs.rsshub.app/routes/traditional-media#ekantipur-%E0%A4%95%E0%A4%BE%E0%A4%A8%E0%A5%8D%E0%A4%A4%E0%A4%BF%E0%A4%AA%E0%A5%81%E0%A4%B0-nepal',
source: ['/:channel'],
target: '/ekantipur/:channel',
},
],
},
};

View File

@ -0,0 +1,3 @@
module.exports = (router) => {
router.get('/:channel?', require('./issue'));
};

View File

@ -1,3 +1,4 @@
module.exports = {
'/myft/:key': ['HenryQW'],
'/:language/:channel?': ['HenryQW', 'xyqfer'],
};

51
lib/v2/ft/myft.js Normal file
View File

@ -0,0 +1,51 @@
const got = require('@/utils/got');
const parser = require('@/utils/rss-parser');
const cheerio = require('cheerio');
module.exports = async (ctx) => {
const ProcessFeed = (content) => {
// clean up the article
content.find('div.o-share, aside, div.o-ads').remove();
return content.html();
};
const link = `https://www.ft.com/myft/following/${ctx.params.key}.rss`;
const feed = await parser.parseURL(link);
const items = await Promise.all(
feed.items.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const response = await got({
method: 'get',
url: item.link,
headers: {
Referer: 'https://www.facebook.com',
},
});
const $ = cheerio.load(response.data);
item.description = ProcessFeed($('article.js-article__content-body'));
item.category = [$('.n-content-tag--with-follow').text()].concat(
$('.article__right-bottom a.concept-list__concept')
.map((i, e) => $(e).text().trim())
.get()
);
item.author = $('a.n-content-tag--author')
.map((i, e) => $(e).text())
.get();
return item;
})
)
);
ctx.state.data = {
title: `FT.com - myFT`,
link,
description: `FT.com - myFT`,
item: items,
};
};

View File

@ -6,6 +6,19 @@ module.exports = {
title: 'FT 中文网',
docs: 'https://docs.rsshub.app/routes/traditional-media#financial-times',
},
{
title: 'myFT 个人 RSS',
docs: 'https://docs.rsshub.app/routes/traditional-media#financial-times',
},
],
},
'ft.com': {
_name: 'Financial Times',
'.': [
{
title: 'myFT personal RSS',
docs: 'https://docs.rsshub.app/routes/en/traditional-media#financial-times',
},
],
},
};

View File

@ -1,3 +1,4 @@
module.exports = function (router) {
router.get('/myft/:key', require('./myft'));
router.get('/:language/:channel?', require('./channel'));
};

41
lib/v2/gofans/index.js Normal file
View File

@ -0,0 +1,41 @@
const got = require('@/utils/got');
const { parseDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
const { join } = require('path');
module.exports = async (ctx) => {
const { kind = '' } = ctx.params;
const baseUrl = 'https://gofans.cn';
const { data: response } = await got('https://api.gofans.cn/v1/web/app_records', {
headers: {
origin: baseUrl,
},
searchParams: {
limit: 20,
kind: kind && (kind === 'macos' ? 1 : 2),
page: 1,
},
});
const items = response.data.map((item) => ({
title: `${item.price === '0.00' ? '免费' : '降价'}」-「${item.kind === 1 ? 'macOS' : 'iOS'}${item.name}`,
description: art(join(__dirname, 'templates/description.art'), {
icon: item.icon,
originalPrice: item.original_price,
price: item.price,
kind: item.kind,
description: item.description.replaceAll('\n', '<br>'),
}),
pubDate: parseDate(item.updated_at, 'X'),
link: new URL(`/app/${item.uuid}`, baseUrl).href,
category: item.primary_genre_name,
}));
ctx.state.data = {
title: '最新限免 / 促销应用',
link: baseUrl,
description: 'GoFans最新限免 / 促销应用',
item: items,
};
};

View File

@ -0,0 +1,3 @@
module.exports = {
'/:kind?': ['HenryQW'],
};

13
lib/v2/gofans/radar.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
'gofans.cn': {
_name: 'GoFans',
'.': [
{
title: '最新限免 / 促销应用',
docs: 'https://docs.rsshub.app/program-update#gofans',
source: ['/limited/:kind?', '/'],
target: (params) => `/gofans${(params.kind && (params.kind === 'macos' ? '/macos' : '/ios')) || ''}`,
},
],
},
};

3
lib/v2/gofans/router.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = (router) => {
router.get('/:kind?', require('./index'));
};

View File

@ -0,0 +1,7 @@
<img src="{{ icon }}">
<br>
原价:¥{{ originalPrice }} -> 现价:¥{{ price }}
<br>
平台:{{ kind === 1 ? 'macOS' : 'iOS' }}
<br/>
{{@ description }}

View File

@ -40,6 +40,7 @@ module.exports = {
'/mof/bond/:category?': ['la3rence'],
'/mofcom/article/:suffix+': ['LogicJake'],
'/moj/aac/news/:type?': ['TonyRL'],
'/moj/lfyjzj': ['la3rence'],
'/mot/:category?': ['nczitzk'],
'/ndrc/fggz/:category?': ['nczitzk'],
'/ndrc/xwdt/:category?': ['nczitzk'],
@ -57,7 +58,7 @@ module.exports = {
'/pbc/tradeAnnouncement': ['nczitzk'],
'/pbc/zcyj': ['Fatpandac'],
'/samr/xgzlyhd/:category?/:department?': ['nczitzk'],
'/safe/bussiness/:site?': ['nczitzk'],
'/safe/business/:site?': ['nczitzk'],
'/safe/complaint/:site?': ['nczitzk'],
'/sasac/:path+': ['TonyRL'],
'/stats/:path+': ['bigfei', 'nczitzk'],

53
lib/v2/gov/moj/lfyjzj.js Normal file
View File

@ -0,0 +1,53 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const timezone = require('@/utils/timezone');
const { parseDate } = require('@/utils/parse-date');
const DOMAIN = 'www.moj.gov.cn';
module.exports = async (ctx) => {
const rootUrl = `https://${DOMAIN}`;
const currentUrl = `${rootUrl}/lfyjzj/lflfyjzj/index.html`;
const { data: response } = await got(currentUrl);
const $ = cheerio.load(response);
const siteName = $('title:first').text();
const theme = $('div.list_title').text();
const description = `${siteName} - ${theme}`;
const icon = new URL('/images/sfbgw_favicon.ico', rootUrl).href;
const indexes = $('ul.newsMsgList_zzy li')
.toArray()
.map((li) => {
const a = $(li).find('a');
const pubDate = $(li).find('div.rightData').text();
const href = a.prop('href');
const link = href.startsWith('http') ? href : new URL(href, currentUrl).href;
return {
title: a.text(),
link,
pubDate: timezone(parseDate(pubDate), +8),
};
});
const items = await Promise.all(
indexes.map((item) =>
ctx.cache.tryGet(`gov:mof:${item.link}`, async () => {
const { data: detailResponse } = await got(item.link);
const content = cheerio.load(detailResponse);
item.description = content('div.TRS_Editor').html();
item.author = content('div.sT_left span:first').text().split('')[1];
const pubDate = content('div.sT_left span:last').text().split('')[1];
item.pubDate = pubDate ? timezone(parseDate(pubDate), +8) : item.pubDate;
return item;
})
)
);
ctx.state.data = {
item: items,
title: theme,
link: currentUrl,
description,
author: siteName,
icon,
};
};

View File

@ -1273,6 +1273,17 @@ module.exports = {
},
],
},
'moj.gov.cn': {
_name: '中华人民共和国司法部',
www: [
{
title: '立法意见征集',
docs: 'https://docs.rsshub.app/routes/government#zhong-hua-ren-min-gong-he-guo-si-fa-bu',
source: ['/lfyjzj/lflfyjzj/*', '/pub/sfbgw/lfyjzj/lflfyjzj/*'],
target: '/gov/moj/lfyjzj',
},
],
},
'moj.gov.tw': {
_name: '台灣法務部廉政署',
'www.aac': [

View File

@ -34,6 +34,7 @@ module.exports = function (router) {
router.get('/mof/bond/:category?', require('./mof/bond'));
router.get('/mofcom/article/:suffix+', require('./mofcom/article'));
router.get('/moj/aac/news/:type?', require('./moj/aac/news'));
router.get('/moj/lfyjzj', require('./moj/lfyjzj'));
router.get('/ndrc/fggz/:category*', require('./ndrc/fggz'));
router.get('/ndrc/xwdt/:category*', require('./ndrc/xwdt'));
router.get('/nea/sjzz/ghs', require('./nea/ghs'));

View File

@ -44,13 +44,13 @@ const processZxfkItems = async (site = 'beijing', category = 'ywzx', limit = '3'
return {
title: `${message.author}: ${message.content}`,
link: new URL(item.find('.title').prop('href'), rootUrl).href,
link: currentUrl,
description: art(path.join(__dirname, 'templates/message.art'), {
message,
reply,
}),
author: `${message.author}/${reply.author}`,
guid: item.find('.id').text(),
guid: `${currentUrl}#${message.author}(${message.date})/${reply.author}(${reply.date})`,
pubDate: parseDate(message.date),
updated: parseDate(reply.date),
};

View File

@ -0,0 +1,47 @@
// 域名
const HOST = 'https://bbs-api-os.hoyolab.com';
// 活动列接口
const EVENT_LIST = '/community/community_contribution/wapi/event/list';
// 活动详情接口
const POST_FULL = '/community/post/wapi/getPostFull';
// 公告和资讯接口
const NEW_LIST = '/community/post/wapi/getNewsList';
const ICON = 'https://img-os-static.hoyolab.com/favicon.ico';
// 源网站链接
const LINK = 'https://www.hoyolab.com';
// 部分图片使用了禁止公开访问的域名
const PRIVATE_IMG = '<img src="https://hoyolab-upload-private.hoyolab.com/upload';
// 使用以下域名可以替换
const PUBLIC_IMG = '<img src="https://upload-os-bbs.hoyolab.com/upload';
// 游戏id
const GIDS_MAP = {
1: 'Honkai Impact 3rd',
2: '原神',
4: '未定事件簿',
5: 'HoYoLAB',
6: '崩坏:星穹铁道',
8: '绝区零',
};
// 公告类型
const TYPE_MAP = {
1: '公告',
2: '活动',
3: '资讯',
};
module.exports = {
PRIVATE_IMG,
PUBLIC_IMG,
LINK,
ICON,
POST_FULL,
HOST,
EVENT_LIST,
NEW_LIST,
GIDS_MAP,
TYPE_MAP,
};

View File

@ -0,0 +1,3 @@
module.exports = {
'/news/:language/:gids/:type': ['ZenoTian'],
};

89
lib/v2/hoyolab/news.js Normal file
View File

@ -0,0 +1,89 @@
const got = require('@/utils/got');
const logger = require('@/utils/logger');
const { parseDate } = require('@/utils/parse-date');
const { HOST, NEW_LIST, TYPE_MAP, POST_FULL, GIDS_MAP, LINK, ICON, PUBLIC_IMG, PRIVATE_IMG } = require('./constant');
const getEventList = async ({ type, gids, size, language }) => {
const query = new URLSearchParams({
type,
gids,
page_size: size,
}).toString();
const url = `${HOST}${NEW_LIST}?${query}`;
const res = await got({
method: 'get',
url,
headers: {
'X-Rpc-Language': language,
},
}).json();
const list = res?.data?.list || [];
return list;
};
const replaceImgDomain = (content) => content.replaceAll(PRIVATE_IMG, PUBLIC_IMG);
const getPostContent = (ctx, list, { type, language }) =>
Promise.all(
list.map(async (row) => {
const post = row.post;
const post_id = post.post_id;
const query = new URLSearchParams({
post_id,
language, // language为了区分缓存对接口并无意义
}).toString();
const url = `${HOST}${POST_FULL}?${query}`;
return await ctx.cache.tryGet(url, async () => {
const res = await got({
method: 'get',
url,
headers: {
'X-Rpc-Language': language,
},
}).json();
const author = res?.data?.post?.user?.nickname || '';
let content = res?.data?.post?.post?.content || '';
if (content === language || !content) {
content = post.content;
}
const description = replaceImgDomain(content);
return {
// 文章标题
title: post.subject,
// 文章链接
link: `${LINK}/article/${post_id}`,
// 文章正文
description,
// 文章发布日期
pubDate: parseDate(post.created_at * 1000),
// 如果有的话,文章分类
category: `${GIDS_MAP[post.game_id]}-${TYPE_MAP[type]}`,
author,
};
});
})
);
module.exports = async (ctx) => {
try {
const { type, gids, language } = ctx.params;
const params = {
type,
gids,
language,
size: parseInt(ctx.query?.limit) || 15,
};
const list = await getEventList(params);
const items = await getPostContent(ctx, list, params);
ctx.state.data = {
title: `HoYoLAB-${GIDS_MAP[gids]}-${TYPE_MAP[type]}`,
link: LINK,
item: items,
image: ICON,
icon: ICON,
logo: ICON,
};
} catch (error) {
logger.error(error);
}
};

22
lib/v2/hoyolab/radar.js Normal file
View File

@ -0,0 +1,22 @@
module.exports = {
'hoyolab.com': {
_name: 'HoYoLAB',
'.': [
{
title: '活动公告资讯',
docs: 'https://docs.rsshub.app/routes/game#hoyolab-news',
source: ['/', '/circles/:gid/:unknow/official'],
target: (params, url) => {
const typeMap = {
notices: '1',
events: '2',
news: '3',
};
const query = new URL(url).searchParams;
const type = typeMap[query.get('page_sort')] || '2';
return `/hoyolab/news/zh-cn/${params?.gid || 2}/${type}`;
},
},
],
},
};

3
lib/v2/hoyolab/router.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = (router) => {
router.get('/news/:language/:gids/:type', require('./news'));
};

View File

@ -0,0 +1,34 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
module.exports = async (ctx) => {
const { body: response } = await got('https://huggingface.co/blog/zh');
const $ = cheerio.load(response);
/** @type {Array<{blog: {local: string, title: string, author: string, thumbnail: string, date: string, tags: Array<string>}, blogUrl: string, lang: 'zh', link: string}>} */
const papers = $('div[data-target="BlogThumbnail"]')
.toArray()
.map((item) => {
const props = $(item).data('props');
const link = $(item).find('a').attr('href');
return {
...props,
link,
};
});
const items = papers.map((item) => ({
title: item.blog.title,
link: `https://huggingface.co${item.link}`,
category: item.blog.tags,
pubDate: parseDate(item.blog.date),
author: item.blog.author,
}));
ctx.state.data = {
title: 'Huggingface 中文博客',
link: 'https://huggingface.co/blog/zh',
item: items,
};
};

View File

@ -1,3 +1,4 @@
module.exports = {
'/blog-zh': ['zcf0508'],
'/daily-papers': ['zeyugao'],
};

View File

@ -8,6 +8,12 @@ module.exports = {
source: ['/papers', '/'],
target: '/huggingface/daily-papers',
},
{
title: '中文博客',
docs: 'https://docs.rsshub.app/routes/programming#huggingface',
source: ['/blog/zh', '/'],
target: '/huggingface/blog-zh',
},
],
},
};

View File

@ -1,3 +1,4 @@
module.exports = (router) => {
router.get('/blog-zh', require('./blog-zh'));
router.get('/daily-papers', require('./daily-papers'));
};

View File

@ -15,62 +15,69 @@ module.exports = async (ctx) => {
const $ = cheerio.load(response);
let items = $('a.logStore, a[aid], div.title a, div.news-header a')
let items = $('a')
.toArray()
.map((item) => {
.reduce((acc, item) => {
item = $(item);
return {
title: item.text(),
link: item.prop('href'),
};
})
.filter((item) => item.link && !item.link.startsWith(new URL('special', rootUrl).href));
const href = item.prop('href');
const link = href ? (href.startsWith('/') ? new URL(href, rootUrl).href : href) : undefined;
if (link && /\/(article|video)\/\w+\.html/.test(link)) {
acc[link] = {
title: item.text(),
link,
};
}
return acc;
}, {});
items = await Promise.all(
items.slice(0, limit).map((item) =>
ctx.cache.tryGet(item.link, async () => {
const { data: detailResponse } = await got(item.link);
Object.values(items)
.slice(0, limit)
.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const { data: detailResponse } = await got(item.link);
const content = cheerio.load(detailResponse);
const image = content('div.article-img img').first();
const video = content('#video-player').first();
const content = cheerio.load(detailResponse);
const image = content('div.article-img img').first();
const video = content('#video-player').first();
item.title = content('div.article-header h1').text();
item.description = art(path.join(__dirname, 'templates/description.art'), {
image: image
? {
src: image.prop('src'),
alt: image.next('p').text() || item.title,
}
: undefined,
video: video
? {
src: video.prop('data-url'),
poster: video.prop('data-poster'),
width: video.prop('width'),
height: video.prop('height'),
}
: undefined,
intro: content('div.article-header p').text(),
description: content('div.article-content').html(),
});
item.author = content('span.author')
.first()
.find('a')
.toArray()
.map((a) => content(a).text())
.join('/');
item.category = content('meta.meta-container a')
.toArray()
.map((c) => content(c).text());
item.pubDate = parseDate(content('div.article-info span[data-article-publish-time]').prop('data-article-publish-time'), 'X');
item.upvotes = content('span.opt-praise__count').text() ? parseInt(content('span.opt-praise__count').text(), 10) : 0;
item.comments = content('span.opt-comment__count').text() ? parseInt(content('span.opt-comment__count').text(), 10) : 0;
item.title = content('div.article-header h1').text();
item.description = art(path.join(__dirname, 'templates/description.art'), {
image: image
? {
src: image.prop('src'),
alt: image.next('p').text() || item.title,
}
: undefined,
video: video
? {
src: video.prop('data-url'),
poster: video.prop('data-poster'),
width: video.prop('width'),
height: video.prop('height'),
}
: undefined,
intro: content('div.article-header p').text(),
description: content('div.article-content').html(),
});
item.author = content('span.author')
.first()
.find('a')
.toArray()
.map((a) => content(a).text())
.join('/');
item.category = content('meta.meta-container a')
.toArray()
.map((c) => content(c).text());
item.pubDate = parseDate(content('div.article-info span[data-article-publish-time]').prop('data-article-publish-time'), 'X');
item.upvotes = content('span.opt-praise__count').text() ? parseInt(content('span.opt-praise__count').text(), 10) : 0;
item.comments = content('span.opt-comment__count').text() ? parseInt(content('span.opt-comment__count').text(), 10) : 0;
return item;
})
)
return item;
})
)
);
const title = $('title').text();

View File

@ -0,0 +1,25 @@
const got = require('@/utils/got');
const getRssItem = require('./utils');
const rootApiUrl = 'https://www.lifeweek.com.cn/api/userWebFollow/getFollowTagContentList?type=3&sort=2&tagId';
const rootUrl = 'https://www.lifeweek.com.cn/column';
const articleRootUrl = 'https://www.lifeweek.com.cn/article';
module.exports = async (ctx) => {
const channel = ctx.params.id;
const url = `${rootApiUrl}=${channel}`;
const { data } = await got(url);
const result = data.model.articleResponseList;
const items = await Promise.all(
result.map((item) => {
const articleLink = `${articleRootUrl}/${item.id}`;
return ctx.cache.tryGet(articleLink, () => getRssItem(item, articleLink));
})
);
ctx.state.data = {
title: data.model.tagName,
link: `${rootUrl}/${channel}`,
item: items,
};
};

View File

@ -0,0 +1,4 @@
module.exports = {
'/channel/:channel': ['changren-wcr'],
'/tag/:tag': ['changren-wcr'],
};

19
lib/v2/lifeweek/radar.js Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
'lifeweek.com.cn': {
_name: '三联生活周刊',
'.': [
{
title: '栏目',
docs: 'https://docs.rsshub.app/routes/traditional-media#san-lian-sheng-huo-zhou-kan',
source: ['/column/:channel'],
target: '/lifeweek/channel/:channel',
},
{
title: '标签',
docs: 'https://docs.rsshub.app/routes/traditional-media#san-lian-sheng-huo-zhou-kan',
source: ['/articleList/:tag'],
target: '/lifeweek/tag/:tag',
},
],
},
};

View File

@ -0,0 +1,4 @@
module.exports = function (router) {
router.get('/channel/:id', require('./channel'));
router.get('/tag/:id', require('./tag'));
};

24
lib/v2/lifeweek/tag.js Normal file
View File

@ -0,0 +1,24 @@
const got = require('@/utils/got');
const getRssItem = require('./utils');
const rootApiUrl = 'https://www.lifeweek.com.cn/api/userWebFollow/getFollowTagContentList?type=4&sort=2&tagId';
const rootUrl = 'https://www.lifeweek.com.cn/articleList';
const articleRootUrl = 'https://www.lifeweek.com.cn/article';
module.exports = async (ctx) => {
const tag = ctx.params.id;
const url = `${rootApiUrl}=${tag}`;
const { data } = await got(url);
const result = data.model.articleResponseList;
const items = await Promise.all(
result.map((item) => {
const articleLink = `${articleRootUrl}/${item.id}`;
return ctx.cache.tryGet(articleLink, () => getRssItem(item, articleLink));
})
);
ctx.state.data = {
title: data.model.tagName,
link: `${rootUrl}/${tag}`,
item: items,
};
};

18
lib/v2/lifeweek/utils.js Normal file
View File

@ -0,0 +1,18 @@
const got = require('@/utils/got');
const timezone = require('@/utils/timezone');
const { parseDate } = require('@/utils/parse-date');
const articleApiRootUrl = 'https://www.lifeweek.com.cn/api/article';
async function getRssItem(item, articleLink) {
const articleApiLink = `${articleApiRootUrl}/${item.id}`;
const { data } = await got(articleApiLink);
const time = timezone(parseDate(item.pubTime), +8);
return {
title: item.title,
description: data.model.content,
link: articleLink,
pubDate: time,
};
}
module.exports = getRssItem;

41
lib/v2/liveuamap/index.js Normal file
View File

@ -0,0 +1,41 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { isValidHost } = require('@/utils/valid-host');
module.exports = async (ctx) => {
let region = ctx.params.region ?? 'ukraine';
const limit = ctx.query.limit ? parseInt(ctx.query.limit) : 50;
if (!isValidHost(region)) {
throw Error('Invalid region');
}
let url = `https://${region}.liveuamap.com/`;
if (region === undefined) {
region = 'Default';
url = 'https://liveuamap.com/';
}
const response = await got({
method: 'get',
url,
});
const $ = cheerio.load(response.data);
const items = $('div#feedler > div')
.slice(0, limit)
.toArray()
.map((item) => {
item = $(item);
return {
title: item.find('div.title').text(),
description: item.find('div.title').text(),
link: item.attr('data-link'),
};
});
ctx.state.data = {
title: `Liveuamap - ${region}`,
link: url,
item: items,
};
};

View File

@ -0,0 +1,3 @@
module.exports = {
'/:region?': ['CoderSherlock'],
};

13
lib/v2/liveuamap/radar.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
'liveuamap.com': {
_name: 'Live Universal Awareness Map',
'.': [
{
title: 'Region',
docs: 'https://docs.rsshub.app/routes/new-media#live-universal-awareness-map',
source: ['/:region*'],
target: '/liveuamap/:region',
},
],
},
};

View File

@ -0,0 +1,3 @@
module.exports = (router) => {
router.get('/:region?', require('./'));
};

View File

@ -0,0 +1,3 @@
module.exports = {
'/': ['artefaritaKuniklo'],
};

View File

@ -0,0 +1,44 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
const timezone = require('@/utils/timezone');
module.exports = async (ctx) => {
const baseUrl = 'https://medieval-china.club';
const { data: response } = await got(baseUrl);
const $ = cheerio.load(response);
const posts = JSON.parse(
$('script:contains("window.localPosts")')
.text()
.match(/window\.localPosts = JSON\.parse\('(.*)'\);/)[1]
)
.slice(0, ctx.query.limit ? parseInt(ctx.query.limit) : 10)
.map((item) => ({
title: item.title,
link: `${baseUrl}${item.path}`,
pubDate: timezone(parseDate(item.date), +8),
}));
const items = await Promise.all(
posts.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const { data: response } = await got(item.link);
const $ = cheerio.load(response);
const imgSrc = $('img').attr('data-original');
$('img').attr('src', `${baseUrl}${imgSrc}`);
$('.head-mask').remove();
$('div.lover-box').remove();
item.description = $('article').first().html();
return item;
})
)
);
ctx.state.data = {
title: '中国的中古',
link: baseUrl,
item: items,
image: 'https://medieval-china.club/images/icons/favicon-144x144.png',
description:
'世界那么大你无法去到每一个地方感受每一处风景时间那么长那些逝去的人你也终将无法与之谋面。而通过古人之文字今人之分享你可以领略以前风光之奇绝瑰玮感受逝人之人情冷暖。中古就是这么一个地方大家来自全球各地不同时区不同性别不同身份不同职业但是大家都被中古的绚烂华章聚集在一起哀其所哀乐其所乐。这是一个虚拟的世界但是我们仿佛跨越千里而来谈一场绝世爱恋今夕何夕仅以此网站献给中古club的每一位成员契阔谈宴西园不芜',
};
};

View File

@ -0,0 +1,13 @@
module.exports = {
'medieval-china.club': {
_name: '中国的中古',
'.': [
{
title: '首页',
docs: 'https://docs.rsshub.app/routes/reading#zhong-guo-de-zhong-shou-ye',
source: '/',
target: '/medieval-china',
},
],
},
};

View File

@ -0,0 +1,3 @@
module.exports = function (router) {
router.get('/', require('./post'));
};

81
lib/v2/ncc-cma/cmdp.js Normal file
View File

@ -0,0 +1,81 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
const path = require('path');
const iconv = require('iconv-lite');
module.exports = async (ctx) => {
const { id } = ctx.params;
const limit = ctx.query.limit ? parseInt(ctx.query.limit, 10) : 50;
const ids = id?.split(/\//) ?? [];
const titles = [];
const rootUrl = 'http://cmdp.ncc-cma.net';
const currentUrl = new URL('cn/index.htm', rootUrl).href;
const { data: response } = await got(currentUrl, {
responseType: 'buffer',
});
const $ = cheerio.load(iconv.decode(response, 'gbk'));
const author = '国家气候中心';
const items = $('ul.img-con-new-con li img[id]')
.toArray()
.filter((item) => ids.length === 0 || ids.includes($(item).prop('id')))
.slice(0, limit)
.map((item) => {
item = $(item);
const id = item.prop('id');
const title = $(`li[data-id="${id}"]`).text() || undefined;
const src = new URL(item.prop('src'), currentUrl).href;
const date =
src
.match(/_(\d{4})(\d{2})(\d{2})_/)
?.slice(1, 4)
.join('-') ?? new Date().toISOString().slice(0, 10);
if (ids.length !== 0 && title) {
titles.push(title);
}
return {
title: `${title} ${date}`,
link: currentUrl,
description: art(path.join(__dirname, 'templates/description.art'), {
image: {
src,
alt: `${title} ${date}`,
},
}),
author,
category: [title],
guid: `ncc-cma#${id}#${date}`,
pubDate: parseDate(date),
enclosure_url: src,
enclosure_type: `image/${src.split(/\./).pop()}`,
};
});
const subtitle = $('h1').last().text();
const image = $('img.logo').prop('src');
const icon = new URL('favicon.ico', rootUrl).href;
ctx.state.data = {
item: items,
title: `${author} - ${subtitle}${titles.length === 0 ? '' : ` - ${titles.join('|')}`}`,
link: currentUrl,
description: $('title').text(),
language: 'zh',
image,
icon,
logo: icon,
subtitle,
author,
allowEmpty: true,
};
};

View File

@ -0,0 +1,3 @@
module.exports = {
'/cmdp/image/:id?': ['nczitzk'],
};

229
lib/v2/ncc-cma/radar.js Normal file
View File

@ -0,0 +1,229 @@
module.exports = {
'ncc-cma.net': {
_name: '国家气候中心',
cmdp: [
{
title: '中国气温 - 日平均气温距平',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/RPJQWQYZ',
},
{
title: '中国气温 - 近5天平均气温距平',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ5TPJQWJP',
},
{
title: '中国气温 - 近10天平均气温距平',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ10TQWJP',
},
{
title: '中国气温 - 近20天平均气温距平',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ20TQWJP',
},
{
title: '中国气温 - 近30天平均气温距平',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ30TQWJP',
},
{
title: '中国气温 - 本月以来气温距平',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/BYYLQWJP',
},
{
title: '中国气温 - 本季以来气温距平',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/BJYLQWJP',
},
{
title: '中国气温 - 本年以来气温距平',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/BNYLQWJP',
},
{
title: '中国降水 - 日降水量分布',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/QGRJSLFBT0808S',
},
{
title: '中国降水 - 近5天降水量',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ5TJSLFBT',
},
{
title: '中国降水 - 近10天降水量',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ10TJSL',
},
{
title: '中国降水 - 近20天降水量',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ20TJSL',
},
{
title: '中国降水 - 近30天降水量',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ30TJSL',
},
{
title: '中国降水 - 本月以来降水量',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/BYYLJSL',
},
{
title: '中国降水 - 本季以来降水量',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/BJYLJSL',
},
{
title: '中国降水 - 近10天降水量距平百分率',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ10TJSLJP',
},
{
title: '中国降水 - 近20天降水量距平百分率',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ20TJSLJP',
},
{
title: '中国降水 - 近30天降水量距平百分率',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/ZJ30TJSLJP',
},
{
title: '中国降水 - 本月以来降水量距平百分率',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/BYYLJSLJPZYQHZ',
},
{
title: '中国降水 - 本季以来降水量距平百分率',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/BJYLJSLJPZJQHZ',
},
{
title: '中国降水 - 本年以来降水量距平百分率',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/BNYLJSLJP',
},
{
title: '全球气温 - 气温距平最近10天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbtmeana10_',
},
{
title: '全球气温 - 气温距平最近20天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbtmeana20_',
},
{
title: '全球气温 - 气温距平最近30天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbtmeana30_',
},
{
title: '全球气温 - 气温距平最近90天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbtmeana90_',
},
{
title: '全球气温 - 最低气温距平最近30天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbtmina30_',
},
{
title: '全球气温 - 最低气温距平最近90天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbtmina90_',
},
{
title: '全球气温 - 最高气温距平最近30天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbtmaxa30_',
},
{
title: '全球气温 - 最高气温距平最近90天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbtmaxa90_',
},
{
title: '全球降水 - 降水量最近10天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbrain10_',
},
{
title: '全球降水 - 降水量最近20天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbrain20_',
},
{
title: '全球降水 - 降水量最近30天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbrain30_',
},
{
title: '全球降水 - 降水量最近90天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbrain90_',
},
{
title: '全球降水 - 降水距平百分率最近10天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbraina10_',
},
{
title: '全球降水 - 降水距平百分率最近20天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbraina20_',
},
{
title: '全球降水 - 降水距平百分率最近30天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbraina30_',
},
{
title: '全球降水 - 降水距平百分率最近90天',
docs: 'https://docs.rsshub.app/routes/forecast#guo-jia-qi-hou-zhong-xin-zui-xin-jian-ce',
source: ['/cn/index.htm'],
target: '/ncc-cma/cmdp/image/glbraina90_',
},
],
},
};

3
lib/v2/ncc-cma/router.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = (router) => {
router.get('/cmdp/image/:id*', require('./cmdp'));
};

View File

@ -0,0 +1,9 @@
{{ if image?.src }}
<figure>
<img
{{ if image.alt }}
alt="{{ image.alt }}"
{{ /if }}
src="{{ image.src }}">
</figure>
{{ /if }}

View File

@ -0,0 +1,3 @@
module.exports = {
'/news': ['Vegann'],
};

56
lib/v2/onet/news.js Normal file
View File

@ -0,0 +1,56 @@
const parser = require('@/utils/rss-parser');
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
const path = require('path');
const { parseArticleContent, parseMainImage } = require('./utils');
module.exports = async (ctx) => {
const rssUrl = 'https://wiadomosci.onet.pl/.feed';
const feed = await parser.parseURL(rssUrl);
const items = await Promise.all(
feed.items.map(async (item) => {
const { description, author, category } = await ctx.cache.tryGet(item.link, async () => {
const { data: response } = await got(item.link, {
headers: {
referer: 'https://www.onet.pl/', // for some reason onet.pl will redirect to the main page if referer is not set
},
});
const $ = cheerio.load(response);
const content = parseArticleContent($);
const mainImage = parseMainImage($);
const description = art(path.join(__dirname, 'templates/article.art'), {
mainImage,
lead: $('#lead').text()?.trim(),
content: content.html()?.trim(),
});
const author = $('.authorNameWrapper span[itemprop="name"]').text()?.trim();
const category = $('span.relatedTopic').text()?.trim();
return { description, author, category };
});
return {
title: item.title,
link: item.link,
description,
author,
category,
pubDate: parseDate(item.pubDate),
guid: item.id,
};
})
);
ctx.state.data = {
title: feed.title,
link: feed.link,
description: feed.title,
item: items,
language: 'pl',
image: 'https://ocdn.eu/wiadomosciucs/static/logo2017/onet2017big_dark.png',
};
};

13
lib/v2/onet/radar.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
'onet.pl': {
_name: 'Onet',
wiadomosci: [
{
title: 'News',
docs: 'https://docs.rsshub.app/routes/new-media#onet',
source: '/',
target: '/onet/news',
},
],
},
};

3
lib/v2/onet/router.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = (router) => {
router.get('/news', require('./news'));
};

View File

@ -0,0 +1,5 @@
{{if lead }}
<p><strong>{{ lead }}</strong></p>
{{/if}}
{{@ mainImage }}
{{@ content }}

View File

@ -0,0 +1,9 @@
<figure>
<img src="{{ url }}" alt="{{ alt }}">
<figcation>
{{if caption }}
<cite>{{ caption }}</cite> -
{{/if}}
{{ author }}
</figcation>
</figure>

51
lib/v2/onet/utils.js Normal file
View File

@ -0,0 +1,51 @@
const { art } = require('@/utils/render');
const path = require('path');
const parseMainImage = ($) => {
const mainImage = $('figure.mainPhoto');
const img = mainImage.find('img');
const author = mainImage.find('span.copyright');
const caption = mainImage.find('span.imageDescription');
return art(path.join(__dirname, 'templates/image.art'), {
url: img.attr('src'),
alt: img.attr('alt')?.trim(),
author: author.text()?.trim(),
caption: caption.text()?.trim(),
});
};
const parseArticleContent = ($) => {
const content = $('[itemprop="articleBody"]');
$('*')
.contents()
.filter(function () {
return this.nodeType === 8;
})
.remove();
content.find('aside').remove();
content.find('.videoPlayerContainer').remove();
content.find('.pulsevideo').remove();
content.find('.adsContainer').remove();
content.find('.placeholder').remove();
content.find('.contentPremium').removeAttr('style');
content.find('div.image').each((i, el) => {
const img = $(el).find('img');
const author = $(el).find('span.author');
const caption = $(el).find('span.caption');
const html = art(path.join(__dirname, 'templates/image.art'), {
url: img.attr('src'),
alt: img.attr('alt')?.trim(),
caption: caption.text()?.trim(),
author: author.text()?.trim(),
});
$(el).replaceWith(html);
});
return content;
};
module.exports = {
parseArticleContent,
parseMainImage,
};

29
lib/v2/otobanana/cast.js Normal file
View File

@ -0,0 +1,29 @@
const got = require('@/utils/got');
const { apiBase, baseUrl, getUserInfo, renderCast } = require('./utils');
module.exports = async (ctx) => {
const { id } = ctx.params;
const userInfo = await getUserInfo(id, ctx.cache.tryGet);
const { data: castData } = await got(`${apiBase}/users/${id}/casts/`);
const casts = castData.results.map((item) => renderCast(item));
ctx.state.data = {
title: `${userInfo.name} (@${userInfo.username}) - 音声投稿 | OTOBANANA`,
description: userInfo.bio.replace(/\n/g, ' '),
link: `${baseUrl}/user/${id}`,
image: userInfo.avatar_url,
icon: userInfo.avatar_url,
logo: userInfo.avatar_url,
language: 'ja',
author: userInfo.name,
itunes_author: userInfo.name,
item: casts,
};
ctx.state.json = {
userInfo,
castData,
};
};

View File

@ -0,0 +1,29 @@
const got = require('@/utils/got');
const { apiBase, baseUrl, getUserInfo, renderLive } = require('./utils');
module.exports = async (ctx) => {
const { id } = ctx.params;
const userInfo = await getUserInfo(id, ctx.cache.tryGet);
const { data: liveData } = await got(`${apiBase}/users/${id}/livestreams/`);
const casts = liveData.results.map((item) => renderLive(item));
ctx.state.data = {
title: `${userInfo.name} (@${userInfo.username}) - ライブ配信 | OTOBANANA`,
description: userInfo.bio.replace(/\n/g, ' '),
link: `${baseUrl}/user/${id}`,
image: userInfo.avatar_url,
icon: userInfo.avatar_url,
logo: userInfo.avatar_url,
language: 'ja',
author: userInfo.name,
itunes_author: userInfo.name,
item: casts,
};
ctx.state.json = {
userInfo,
liveData,
};
};

View File

@ -0,0 +1,5 @@
module.exports = {
'/user/:id': ['TonyRL'],
'/user/:id/cast': ['TonyRL'],
'/user/:id/livestream': ['TonyRL'],
};

25
lib/v2/otobanana/radar.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
'otobanana.com': {
_name: 'OTOBANANA',
'.': [
{
title: 'Timeline タイムライン',
docs: 'https://docs.rsshub.app/multimedia#otobanana',
source: ['/user/:id'],
target: '/otobanana/user/:id',
},
{
title: 'Cast 音声投稿',
docs: 'https://docs.rsshub.app/multimedia#otobanana',
source: ['/user/:id/cast', '/user/:id'],
target: '/otobanana/user/:id/cast',
},
{
title: 'Livestream ライブ配信',
docs: 'https://docs.rsshub.app/multimedia#otobanana',
source: ['/user/:id/livestream', '/user/:id'],
target: '/otobanana/user/:id/livestream',
},
],
},
};

View File

@ -0,0 +1,5 @@
module.exports = (router) => {
router.get('/user/:id', require('./timeline'));
router.get('/user/:id/cast', require('./cast'));
router.get('/user/:id/livestream', require('./livestream'));
};

View File

@ -0,0 +1,11 @@
{{ if cast }}
<img src="{{ cast.thumbnail_url }}">
<br>
<audio controls>
<source src="{{ cast.audio_url }}" type="audio/x-m4a">
</audio>
<br>
💬 {{ cast.comment_count }} ❤️ {{ cast.like_count }} 🍌 {{ cast.gift_banana }} {{ cast.play_count }} 再生
<br>
{{@ cast.text.replace(/\n/g, '<br>') }}
{{ /if }}

View File

@ -0,0 +1,29 @@
const got = require('@/utils/got');
const { apiBase, baseUrl, getUserInfo, renderPost } = require('./utils');
module.exports = async (ctx) => {
const { id } = ctx.params;
const userInfo = await getUserInfo(id, ctx.cache.tryGet);
const { data: postData } = await got(`${apiBase}/users/${id}/posts/`);
const posts = postData.results.map((item) => renderPost(item));
ctx.state.data = {
title: `${userInfo.name} (@${userInfo.username}) - タイムライン | OTOBANANA`,
description: userInfo.bio.replace(/\n/g, ' '),
link: `${baseUrl}/user/${id}`,
image: userInfo.avatar_url,
icon: userInfo.avatar_url,
logo: userInfo.avatar_url,
language: 'ja',
author: userInfo.name,
itunes_author: userInfo.name,
item: posts,
};
ctx.state.json = {
userInfo,
postData,
};
};

Some files were not shown because too many files have changed in this diff Show More