215 lines
8.0 KiB
TypeScript
215 lines
8.0 KiB
TypeScript
import { load } from 'cheerio';
|
|
import queryString from 'query-string';
|
|
|
|
import { config } from '@/config';
|
|
import type { Route } from '@/types';
|
|
import cache from '@/utils/cache';
|
|
import got from '@/utils/got';
|
|
import { parseDate, parseRelativeDate } from '@/utils/parse-date';
|
|
import parser from '@/utils/rss-parser';
|
|
import timezone from '@/utils/timezone';
|
|
|
|
const threadIdRegex = /(\d+)-\d+-\d+/;
|
|
const header = {
|
|
Cookie: config.keylol.cookie ?? undefined,
|
|
};
|
|
|
|
export const route: Route = {
|
|
path: '/:path',
|
|
name: '论坛',
|
|
parameters: { path: '路径,默认为热点聚焦' },
|
|
categories: ['game'],
|
|
example: '/keylol/f161-1',
|
|
features: {
|
|
requireConfig: [
|
|
{
|
|
name: 'KEYLOL_COOKIE',
|
|
optional: true,
|
|
description: `配置后可抓取具有阅读权限的帖子內容`,
|
|
},
|
|
],
|
|
requirePuppeteer: false,
|
|
antiCrawler: false,
|
|
supportBT: false,
|
|
supportPodcast: false,
|
|
supportScihub: false,
|
|
},
|
|
radar: [
|
|
{
|
|
source: ['keylol.com/:path'],
|
|
target: (params, url) => url.replaceAll('forum.php?', ''),
|
|
},
|
|
],
|
|
maintainers: ['nczitzk', 'kennyfong19931'],
|
|
handler,
|
|
description: `::: tip
|
|
若订阅 [热点聚焦](https://keylol.com/f161-1),网址为 \`https://keylol.com/f161-1\`。截取 \`https://keylol.com/\` 到末尾的部分 \`f161-1\` 作为参数,此时路由为 [\`/keylol/f161-1\`](https://rsshub.app/keylol/f161-1)。
|
|
若订阅子分类 [试玩免费 - 热点聚焦](https://keylol.com/forum.php?mod=forumdisplay&fid=161&filter=typeid&typeid=459),网址为 \`https://keylol.com/forum.php?mod=forumdisplay&fid=161&filter=typeid&typeid=459\`。提取\`fid\`及\`typeid\` 作为参数,此时路由为 [\`/keylol/fid=161&typeid=459\`](https://rsshub.app/keylol/fid=161&typeid=459)。注意不要包括\`filter\`,会调用[全局的内容过滤](https://docs.rsshub.app/guide/parameters#filtering)。
|
|
:::`,
|
|
};
|
|
|
|
async function handler(ctx) {
|
|
let queryParams = {};
|
|
const path = ctx.req.param('path');
|
|
if (/^f\d+-\d+/.test(path)) {
|
|
queryParams.fid = path.match(/^f(\d+)-\d+/)[1];
|
|
} else {
|
|
queryParams = queryString.parse(path);
|
|
}
|
|
queryParams.mod = 'forumdisplay';
|
|
queryParams.orderby = 'dateline';
|
|
queryParams.filter = 'author';
|
|
|
|
// get real author name from official rss feed
|
|
let authorNameMap;
|
|
try {
|
|
const feed = await parser.parseURL(`https://keylol.com/forum.php?mod=rss&fid=${queryParams.fid}&auth=0`);
|
|
authorNameMap = feed.items.map((item) => ({
|
|
threadId: item.link.match(threadIdRegex)[1],
|
|
author: item.author,
|
|
}));
|
|
} catch {
|
|
authorNameMap = [];
|
|
}
|
|
|
|
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30;
|
|
|
|
const rootUrl = 'https://keylol.com';
|
|
const currentUrl = queryString.stringifyUrl({ url: `${rootUrl}/forum.php`, query: queryParams });
|
|
|
|
const { data: response } = await got({
|
|
method: 'get',
|
|
url: currentUrl,
|
|
headers: header,
|
|
});
|
|
|
|
const $ = load(response);
|
|
|
|
let items = $('tbody[id^="normalthread_"]')
|
|
.slice(0, limit)
|
|
.toArray()
|
|
.map((item) => {
|
|
item = $(item);
|
|
|
|
return {
|
|
title: item.find('a.xst').text(),
|
|
link: new URL(item.find(' a.xst').prop('href').split('&extra=')[0], rootUrl).href,
|
|
author: item.find('td.by-author cite').text(),
|
|
pubDate: parseRelativeDate(item.find('td.by-author em').text().replaceAll(' 发表', '')),
|
|
};
|
|
});
|
|
|
|
items = await Promise.all(
|
|
items.map((item) =>
|
|
cache.tryGet(item.link, async () => {
|
|
const threadId = threadIdRegex.test(item.link) ? item.link.match(threadIdRegex)[1] : queryString.parseUrl(item.link).query.tid;
|
|
const { data: detailResponse } = await got({
|
|
method: 'get',
|
|
url: item.link,
|
|
headers: header,
|
|
});
|
|
|
|
const content = load(detailResponse);
|
|
|
|
let descriptionList: string[] = [];
|
|
const indexDiv = content('div#threadindex');
|
|
if (indexDiv.length > 0) {
|
|
// post with page
|
|
const postId = content('div.t_fsz > script')
|
|
.text()
|
|
.match(/show_threadindex\((\d+),/)[1];
|
|
descriptionList = await Promise.all(
|
|
indexDiv.find('a').map((i, a) => {
|
|
const pageTitle = $(a).text();
|
|
const page = $(a).attr('page');
|
|
return getPage(
|
|
`${rootUrl}/forum.php?${queryString.stringify({
|
|
mod: 'viewthread',
|
|
tid: threadId,
|
|
viewpid: postId,
|
|
cp: page,
|
|
})}`,
|
|
pageTitle
|
|
);
|
|
})
|
|
);
|
|
} else {
|
|
// normal post
|
|
descriptionList.push(getDescription(content));
|
|
}
|
|
|
|
item.description = descriptionList.join('<br/>');
|
|
const realAuthorName = authorNameMap.find((a) => a.threadId === threadId);
|
|
if (realAuthorName) {
|
|
item.author = realAuthorName.author;
|
|
}
|
|
item.category = content('#keyloL_thread_tags a')
|
|
.toArray()
|
|
.map((c) => content(c).text());
|
|
|
|
const pubDateEm = content('img.authicn').first().next();
|
|
const pubDateText = pubDateEm.find('span').prop('title') ?? pubDateEm.text();
|
|
const pubDateMatches = pubDateText.match(/(\d{4}(?:-\d{1,2}){2} (?:\d{2}:){2}\d{2})/) ?? undefined;
|
|
if (pubDateMatches) {
|
|
item.pubDate = timezone(parseDate(pubDateMatches[1], 'YYYY-M-D HH:mm:ss'), +8);
|
|
}
|
|
|
|
const updatedMatches =
|
|
content('i.pstatus')
|
|
.text()
|
|
.match(/(\d{4}(?:-\d{1,2}){2} (?:\d{2}:){2}\d{2})/) ?? undefined;
|
|
if (updatedMatches) {
|
|
item.updated = timezone(parseDate(updatedMatches[1], 'YYYY-M-D HH:mm:ss'), +8);
|
|
}
|
|
|
|
item.comments = content('div.subforum_right_title_left_down').text() ? Number.parseInt(content('div.subforum_right_title_left_down').text(), 10) : 0;
|
|
|
|
return item;
|
|
})
|
|
)
|
|
);
|
|
|
|
const icon = $('link[rel="apple-touch-icon"]').prop('href');
|
|
|
|
return {
|
|
item: items,
|
|
title: $('title').text(),
|
|
link: currentUrl,
|
|
description: $('meta[name="description"]').prop('content'),
|
|
language: 'zh-cn',
|
|
icon,
|
|
logo: icon,
|
|
subtitle: $('meta[name="application-name"]').prop('content'),
|
|
author: $('meta[name="author"]').prop('content'),
|
|
};
|
|
}
|
|
|
|
function getDescription($) {
|
|
const descriptionEl = $('td.t_f');
|
|
descriptionEl.find('div.rnd_ai_pr').remove(); // remove ad image
|
|
|
|
// handle lazyload image
|
|
descriptionEl.find('img').each((_, img) => {
|
|
img = $(img);
|
|
if (img.attr('src')?.endsWith('none.gif') && img.attr('file')) {
|
|
img.attr('src', img.attr('file'));
|
|
img.removeAttr('file');
|
|
img.removeAttr('zoomfile');
|
|
}
|
|
});
|
|
|
|
return descriptionEl.length > 0 ? descriptionEl.html() : $('div.alert_info').html();
|
|
}
|
|
|
|
async function getPage(url, pageTitle) {
|
|
const { data: detailResponse } = await got({
|
|
method: 'get',
|
|
url,
|
|
headers: header,
|
|
});
|
|
|
|
const $ = load(detailResponse, { xmlMode: true });
|
|
const content = $('root').text();
|
|
return '<h3>' + pageTitle + '</h3>' + getDescription(load(content));
|
|
}
|