Merge remote-tracking branch 'origin/master' into feature/cloudflare-workers

This commit is contained in:
DIYgod
2024-03-22 22:44:02 +08:00
27 changed files with 761 additions and 528 deletions

View File

@@ -11,9 +11,6 @@ updates:
ignore:
- dependency-name: jsrsasign
versions: ['>=11.0.0'] # no longer includes KJUR.crypto.Cipher for RSA
# ESM only packages
- dependency-name: got
versions: ['>=12.0.0']
- dependency-name: unified
versions: ['>=10.0.0']

View File

@@ -47,23 +47,20 @@ jobs:
publish_dir: ./assets
user_name: 'github-actions[bot]'
user_email: '41898282+github-actions[bot]@users.noreply.github.com'
- name: Pushes to docs repository - en
uses: cpina/github-action-push-to-another-repository@main
env:
API_TOKEN_GITHUB: ${{ secrets.DOCS_API_TOKEN }}
- name: Checkout docs
uses: actions/checkout@v4
with:
source-directory: './assets/build/docs/en'
target-directory: './src/routes'
destination-github-username: 'RSSNext'
destination-repository-name: 'rsshub-docs'
target-branch: 'main'
- name: Pushes to docs repository - zh
uses: cpina/github-action-push-to-another-repository@main
env:
API_TOKEN_GITHUB: ${{ secrets.DOCS_API_TOKEN }}
with:
source-directory: './assets/build/docs/zh'
target-directory: './src/zh/routes'
destination-github-username: 'RSSNext'
destination-repository-name: 'rsshub-docs'
target-branch: 'main'
repository: 'RSSNext/rsshub-docs'
token: ${{ secrets.DOCS_API_TOKEN }}
path: rsshub-docs
- name: Update docs
run: |
cp -r ./assets/build/docs/en/* ./rsshub-docs/src/routes
cp -r ./assets/build/docs/zh/* ./rsshub-docs/src/zh/routes
- name: Commit docs
run: |
cd rsshub-docs
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git status
git diff-index --quiet HEAD || (git commit -m "chore: auto build https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA" -a --no-verify && git push "https://${GITHUB_ACTOR}:${{ secrets.DOCS_API_TOKEN }}@github.com/RSSNext/rsshub-docs.git" HEAD:main)

View File

@@ -4,10 +4,6 @@ on:
push:
branches:
- master
workflow_run:
workflows: ['Build assets']
types:
- completed
permissions:
contents: read

View File

@@ -80,6 +80,10 @@ jobs:
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-route/identify.js`)
return script({ github, context, core }, body, number, sender)
- name: Build RSSHub
if: env.TEST_CONTINUE
run: pnpm build
- name: Start RSSHub
if: env.TEST_CONTINUE
run: pnpm start &

View File

@@ -756,8 +756,8 @@ router.get('/nosetime/home', lazyloadRouteHandler('./routes/nosetime/home'));
router.get('/daxiaamu/home', lazyloadRouteHandler('./routes/daxiaamu/home'));
// 爱发电
router.get('/afdian/explore/:type?/:category?', lazyloadRouteHandler('./routes/afdian/explore'));
router.get('/afdian/dynamic/:uid', lazyloadRouteHandler('./routes/afdian/dynamic'));
// router.get('/afdian/explore/:type?/:category?', lazyloadRouteHandler('./routes/afdian/explore'));
// router.get('/afdian/dynamic/:uid', lazyloadRouteHandler('./routes/afdian/dynamic'));
// Simons Foundation
router.get('/simonsfoundation/articles', lazyloadRouteHandler('./routes/simonsfoundation/articles'));

View File

@@ -26,9 +26,9 @@ export const route: Route = {
url: 'www.2023game.com/',
description: `分类
| PS4游戏 | switch游戏 | 3DS游戏 | PSV游戏 | Xbox360 | PS3游戏 | 世嘉MD/SS | PSP游戏 | PC周边 | 怀旧掌机 | 怀旧主机 | PS4教程 | PS4金手指 | switch金手指 | switch教程 | switch补丁 | switch主题 | switch存档 |
| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
| ps4 | sgame | 3ds | psv | jiaocheng | ps3yx | zhuji.md | zhangji.psp | pcgame | zhangji | zhuji | ps4.psjc | ps41.ps4pkg | nsaita.cundang | nsaita.pojie | nsaita.buding | nsaita.zhutie | nsaita.zhuti |`,
| PS4游戏 | switch游戏 | 3DS游戏 | PSV游戏 | Xbox360 | PS3游戏 | 世嘉MD/SS | PSP游戏 | PC周边 | 怀旧掌机 | 怀旧主机 | PS4教程 | PS4金手指 | switch金手指 | switch教程 | switch补丁 | switch主题 | switch存档 |
| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
| ps4 | sgame | 3ds | psv | jiaocheng | ps3yx | zhuji.md | zhangji.psp | pcgame | zhangji | zhuji | ps4.psjc | ps41.ps4pkg | nsaita.cundang | nsaita.pojie | nsaita.buding | nsaita.zhutie | nsaita.zhuti |`,
};
async function handler(ctx?: Context): Promise<Data> {

View File

@@ -17,10 +17,19 @@ const shortcuts = {
};
export const route: Route = {
path: '/{.*}?',
path: '/:category/:subCategory?/:keyword?',
categories: ['new-media'],
example: '/36kr/newsflashes',
parameters: {
category: '分类,必填项',
subCategory: '子分类,选填项,目的是为了兼容老逻辑',
keyword: '关键词,选填项,仅搜索文章/快讯时有效',
},
name: '资讯, 快讯, 用户文章, 主题文章, 专题文章, 搜索文章, 搜索快讯',
maintainers: ['nczitzk'],
maintainers: ['nczitzk', 'fashioncj'],
description: `| 最新资讯频道 | 快讯 | 推荐资讯|生活|房产|职场|搜索文章|搜索快讯|
| ------- | -------- -------- -------- -------- -------- -------- --------
| news | newsflashes recommend life estate workplace search/articles/关键词 search/articles/关键词 `,
handler,
};

View File

@@ -6,7 +6,7 @@ import { parseDate } from '@/utils/parse-date';
import { parseArticle } from './utils';
export const route: Route = {
path: '/:name/:type?',
path: '/games/:name/:type?',
radar: [
{
source: ['3dmgame.com/games/:name/:type'],
@@ -14,7 +14,7 @@ export const route: Route = {
],
name: '游戏资讯',
categories: ['game'],
maintainers: ['sinchang', 'jacky2001114', 'HenryQW'],
maintainers: ['sinchang', 'jacky2001114', 'HenryQW', 'lyqluis'],
handler,
};

View File

@@ -25,7 +25,7 @@ export const route: Route = {
},
],
name: '新闻中心',
maintainers: ['zhboner'],
maintainers: ['zhboner', 'lyqluis'],
handler,
description: `| 新闻推荐 | 游戏新闻 | 动漫影视 | 智能数码 | 时事焦点 |
| -------- | -------- | -------- | -------- | ----------- |
@@ -35,7 +35,7 @@ export const route: Route = {
async function handler(ctx) {
const { category = '' } = ctx.req.param();
const isArcPost = category && !isNaN(category); // https://www.3dmgame.com/news/\d+/
const url = `https://www.3dmgame.com/${category && category !== 'news_36_1' ? 'news/' : ''}${category ?? 'news'}/`;
const url = `https://www.3dmgame.com/${category === 'news_36_1' ? category : 'news/' + category}`;
const res = await got(url);
const $ = load(res.data);
const list = $(isArcPost ? '.selectarcpost' : '.selectpost')

View File

@@ -1,7 +1,17 @@
const got = require('@/utils/got');
import got from '@/utils/got';
module.exports = async (ctx) => {
const url_slug = ctx.params.uid.replace('@', '');
export const route: Route = {
path: '/dynamic/:uid?',
categories: ['other'],
example: '/afdian/dynamic/@afdian',
parameters: { uid: '用户id用户动态页面url里可找到' },
name: '用户动态',
maintainers: ['sanmmm'],
handler,
};
async function handler(ctx) {
const url_slug = ctx.req.param('uid').replace('@', '');
const baseUrl = 'https://afdian.net';
const userInfoRes = await got(`${baseUrl}/api/user/get-profile-by-slug`, {
searchParams: {
@@ -26,11 +36,11 @@ module.exports = async (ctx) => {
pubDate: new Date(Number(publish_time) * 1000).toUTCString(),
};
});
ctx.state.data = {
return {
title: `${name}的爱发电动态`,
description: `${name}的爱发电动态`,
image: avatar,
link: `${baseUrl}/@${url_slug}`,
item: list,
};
};
}

View File

@@ -1,4 +1,4 @@
const got = require('@/utils/got');
import got from '@/utils/got';
const categoryMap = {
: '',
@@ -26,8 +26,29 @@ const typeToLabel = {
hot: '人气',
};
module.exports = async (ctx) => {
const { type = 'rec', category = '所有' } = ctx.params;
export const route: Route = {
path: '/explore/:type/:category?',
categories: ['other'],
example: '/afdian/explore/hot/所有',
parameters: { type: '分类', category: '目录类型,默认为 `所有`' },
name: '发现用户',
maintainers: ['sanmmm'],
description: `分类
| | |
| ---- | ---- |
| rec | hot |
| | | | | | | | | | Vtuber | | | | | | | | |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| | | | | | | | | | Vtuber | | | | | | | | |`,
handler,
};
async function handler(ctx) {
const { type = 'rec', category = '所有' } = ctx.req.param();
const baseUrl = 'https://afdian.net';
const link = `${baseUrl}/api/creator/list`;
const res = await got(link, {
@@ -48,10 +69,10 @@ module.exports = async (ctx) => {
link: `${baseUrl}/@${item.url_slug}`,
};
});
ctx.state.data = {
return {
title: `爱发电-创作者 (按 ${category}/${typeToLabel[type]})`,
description: `爱发电-发现创作者 (按 ${category}/${typeToLabel[type]})`,
link: `${baseUrl}/explore`,
item: list,
};
};
}

View File

@@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '爱发电',
url: 'afdian.net',
};

View File

@@ -20,8 +20,7 @@ export const route: Route = {
},
name: '新书速递',
maintainers: ['fengkx', 'lyqluis'],
description: `
| 文学 | 小说 | 历史文化 | 社会纪实 | 科学新知 | 艺术设计 | 商业经管 | 绘本漫画 |
description: `| 文学 | 小说 | 历史文化 | 社会纪实 | 科学新知 | 艺术设计 | 商业经管 | 绘本漫画 |
| ------------ | ------- | -------- | --------- | -------- | -------- | -------- | -------- |
| prose_poetry | fiction | history | biography | science | art | business | comics |`,
handler,

View File

@@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '子方有料',
url: 'ippa.top',
};

35
lib/routes/ippa/rss.ts Normal file
View File

@@ -0,0 +1,35 @@
import { Route } from '@/types';
import { parseDate } from '@/utils/parse-date';
import got from '@/utils/got';
export const route: Route = {
path: '/',
categories: ['blog'],
example: '/ippa',
radar: [
{
source: ['ippa.top/'],
},
],
name: '最新文章',
maintainers: ['cnkmmk'],
handler,
url: 'ippa.top/',
};
async function handler() {
const url = 'https://www.ippa.top';
const response = await got(`${url}/wp-json/wp/v2/posts`);
const list = response.data;
return {
title: '子方有料',
link: url,
description: '子方有料 - 最新文章',
item: list.map((item) => ({
title: item.title.rendered,
link: item.link,
pubDate: parseDate(item.date_gmt),
description: item.content.rendered,
})),
};
}

View File

@@ -5,7 +5,7 @@ import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/news/:language?',
categories: ['new-media'],
example: '/news/zh-hans, /news/zh-hant',
example: '/news/zh-hans',
parameters: {
language: '语言',
},

View File

@@ -1,6 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '哔轻小说',
name: '哔轻小说',
url: 'linovelib.com',
};

View File

@@ -0,0 +1,36 @@
import type { Route, Data } from '@/types';
import type { Context } from 'hono';
import got from '@/utils/got';
import { load } from 'cheerio';
export const route: Route = {
path: '/volume/:id',
categories: ['reading'],
example: '/linovelib/volume/8',
parameters: { id: '小说 ID可在小说页 URL 中找到' },
radar: [
{
source: ['www.linovelib.com/novel/:id/catalog'],
},
],
name: '卷',
maintainers: ['rkscv'],
handler,
};
async function handler(ctx: Context): Promise<Data> {
const { id } = ctx.req.param();
const link = `https://www.linovelib.com/novel/${id}/catalog`;
const $ = load(await got(link).text());
return {
title: `${$('.book-meta h1').text()} - 哔哩轻小说`,
link,
item: $('.volume')
.toArray()
.map((elem) => ({
title: $(elem).find('h2').text(),
link: $(elem).find('.volume-cover').attr('href'),
}))
.toReversed(),
};
}

View File

@@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'LA UOSC社区',
url: 'loongarch.org',
};

View File

@@ -0,0 +1,59 @@
import { parseDate } from '@/utils/parse-date';
import got from '@/utils/got';
export const route: Route = {
path: '/post/:type?',
categories: ['bbs'],
example: '/loongarch/post/newest',
parameters: { type: 'top 或 newest' },
radar: [
{
source: ['bbs.loongarch.org'],
},
],
name: '最热 / 最新帖子',
maintainers: ['ladeng07', '3401797899'],
handler,
url: 'bbs.loongarch.org/',
};
async function handler(ctx) {
const type = ctx.req.param('type');
const link = 'https://bbs.loongarch.org/api/discussions';
let title = '最新帖子';
let sortValue = '-createdAt';
if (type === 'top') {
title = '最热帖子';
sortValue = '-commentCount';
}
const { data: response } = await got('https://bbs.loongarch.org/api/discussions', {
searchParams: {
include: 'user,tags,tags.parent,firstPost',
sort: sortValue,
'page[offset]': 0,
},
});
const users = response.included.filter((i) => i.type === 'users');
const tags = response.included.filter((i) => i.type === 'tags');
const posts = response.included.filter((i) => i.type === 'posts');
const out = response.data.map(({ attributes, relationships }) => ({
title: attributes.title,
link: `https://bbs.loongarch.org/d/${attributes.slug}`,
author: users.find((i) => i.id === relationships.user.data.id).attributes.displayName,
description: posts.find((i) => i.id === relationships.firstPost.data.id).attributes.contentHtml,
pubDate: parseDate(attributes.createdAt),
updated: parseDate(attributes.lastPostedAt),
category: relationships.tags.data.map((tag) => tags.find((i) => i.id === tag.id).attributes.name),
}));
return {
title: `LA UOSC-${title}`,
link,
description: `LA UOSC-${title}`,
item: out,
};
}

View File

@@ -1,28 +1,19 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
import { parseDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
const baseUrl = 'https://xd.x6d.com';
import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/:id?',
categories: ['new-media'],
example: '/x6d/34',
parameters: { id: '分类 id可在对应分类页面的 URL 中找到,默认为首页最近更新' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '分类',
url: 'xd.x6d.com',
maintainers: ['nczitzk'],
handler,
example: '/x6d/34',
parameters: { id: '分类 id可在对应分类页面的 URL 中找到,默认为首页最近更新' },
description: `| 技巧分享 | QQ 技巧 | 微信技巧 | 其他教程 | 其他分享 |
| -------- | ------- | -------- | -------- | -------- |
| 31 | 55 | 112 | 33 | 88 |
@@ -46,54 +37,105 @@ export const route: Route = {
| 资源宝库 | 书籍资料 | 设计资源 | 剪辑资源 | 办公资源 | 壁纸资源 | 编程资源 |
| -------- | -------- | -------- | -------- | -------- | -------- | -------- |
| 106 | 107 | 108 | 109 | 110 | 111 | 113 |`,
categories: ['new-media'],
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
};
async function handler(ctx) {
export async function handler(ctx) {
const { id = 'latest' } = ctx.req.param();
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 22;
const currentUrl = id === 'latest' ? baseUrl : `${baseUrl}/html/${id}.html`;
const rootUrl = 'https://xd.x6d.com';
const { data: response } = await got(currentUrl);
let currentUrl = new URL(id === 'latest' ? '' : `html/${id}.html`, rootUrl).href;
const $ = load(response);
const { data: firstResponse } = await got(currentUrl);
let $;
if (/<meta\s/.test(firstResponse)) {
$ = load(firstResponse);
} else {
currentUrl = new URL(
id === 'latest'
? ''
: firstResponse
.match(/'([\w./=?]+)'/g)
.reverse()
.join('')
.replaceAll("'", ''),
rootUrl
).href;
const { data: response } = await got(currentUrl);
$ = load(response);
}
$('i.rj').remove();
const query =
id === 'latest'
? $('#newslist ul')
.eq(0)
.find('li')
.not('.addd')
.find('a')
.slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 22)
: $('a.soft-title').slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10);
const language = 'zh';
const list = query.toArray().map((item) => {
const query = id === 'latest' ? $('#newslist ul').first().find('li').not('li.addd').find('a').slice(0, limit) : $('a.soft-title').slice(0, limit);
let items = query.toArray().map((item) => {
item = $(item);
return {
title: item.text(),
link: `${baseUrl}${item.attr('href')}`,
title: item.prop('title') ?? item.text(),
link: new URL(item.prop('href'), rootUrl).href,
language,
};
});
const items = await Promise.all(
list.map((item) =>
items = await Promise.all(
items.map((item) =>
cache.tryGet(item.link, async () => {
const { data: detailResponse } = await got(item.link);
const content = load(detailResponse);
item.description = content('div.article-content').html();
item.pubDate = timezone(parseDate(content('time').text()), 8);
const $$ = load(detailResponse);
const title = $$('h1.article-title').text();
const description = $$('div.article-content').html();
const image = new URL($$('div.article-content img').first().prop('src'), rootUrl).href;
item.title = title;
item.description = description;
item.pubDate = timezone(parseDate($$('time').text()), +8);
item.category = $$('b.bq-wg')
.toArray()
.map((c) => $$(c).text());
item.author = $$('span.bq-zz').text();
item.content = {
html: description,
text: $$('div.article-content').text(),
};
item.image = image;
item.banner = image;
item.language = language;
return item;
})
)
);
const image = new URL($('div.header-logo img').prop('src'), rootUrl).href;
return {
title: `小刀娱乐网 - ${$('title').text().split('-')[0]}`,
title: $('title').text().split(/\s-/)[0],
description: $('meta[name="description"]').prop('content'),
link: currentUrl,
item: items,
allowEmpty: true,
image,
language,
};
}

View File

@@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '甬哥侃侃侃YouTube教程摘要随笔',
url: 'ygkkk.blogspot.com',
};

48
lib/routes/ygkkk/rss.ts Normal file
View File

@@ -0,0 +1,48 @@
import { Route } from '@/types';
import got from '@/utils/got';
import { load } from 'cheerio';
export const route: Route = {
path: '/',
categories: ['blog'],
example: '/ygkkk',
radar: [
{
source: ['ygkkk.blogspot.com/'],
},
],
name: '最新发表',
maintainers: ['cnkmmk'],
handler,
url: 'ygkkk.blogspot.com/',
};
async function handler() {
const currentUrl = 'https://ygkkk.blogspot.com';
const response = await got(`${currentUrl}/feeds/posts/default?alt=rss`);
const $ = load(response.data, { xmlMode: true });
const title_main = $('channel > title').text();
const description_main = $('channel > description').text();
const items = $('channel > item')
.map((_, item) => {
const $item = $(item);
const link = $item.find('link').text();
const title = $item.find('title').text();
const description = $item.find('description').text();
const pubDate = $item.find('pubDate').text();
return {
link,
pubDate, // no need to normalize because it's from a valid RSS feed
title,
description,
};
})
.get();
return {
title: title_main,
description: description_main,
link: currentUrl,
item: items,
};
}

View File

@@ -1,6 +1,6 @@
import logger from '@/utils/logger';
import { config } from '@/config';
import got, { CancelableRequest, Response as GotResponse, NormalizedOptions, Options, Got } from 'got';
import got, { CancelableRequest, Response as GotResponse, OptionsInit, Options, Got } from 'got';
type Response<T> = GotResponse<string> & {
data: T;
@@ -25,22 +25,14 @@ const custom: {
delete: GotRequestFunction;
} & GotRequestFunction &
Got = got.extend({
get: got.get,
retry: {
limit: config.requestRetry,
statusCodes: [400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 421, 422, 423, 424, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, 521, 522, 524],
},
hooks: {
beforeRetry: [
(
options: NormalizedOptions & {
retryCount?: number;
},
err,
count
) => {
logger.error(`Request ${options.url} fail, retry attempt #${count}: ${err}`);
options.retryCount = count;
(err, count) => {
logger.error(`Request ${err.options.url} fail, retry attempt #${count}: ${err}`);
},
],
beforeRedirect: [
@@ -63,7 +55,7 @@ const custom: {
],
init: [
(
options: Options & {
options: OptionsInit & {
data?: string;
}
) => {
@@ -84,4 +76,4 @@ const custom: {
custom.all = (list) => Promise.all(list);
export default custom;
export type { Response, NormalizedOptions, Options } from 'got';
export type { Response, Options } from 'got';

View File

@@ -45,6 +45,7 @@ const requestWrapper = (url: string, options: http.RequestOptions = {}) => {
if (config.proxyStrategy === 'all') {
prxied = proxyWrapper(url, optionsWithHeaders);
} else if (config.proxyStrategy === 'on_retry' && (optionsWithHeaders as any).retryCount) {
// TODO
prxied = proxyWrapper(url, optionsWithHeaders);
}
if (prxied) {

View File

@@ -72,7 +72,7 @@
"fanfou-sdk": "5.0.0",
"git-rev-sync": "3.0.2",
"googleapis": "134.0.0",
"got": "11.8.6",
"got": "14.2.1",
"hono": "4.1.3",
"html-to-text": "9.0.5",
"https-proxy-agent": "7.0.4",
@@ -157,7 +157,7 @@
"eslint-plugin-n": "16.6.2",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-unicorn": "51.0.1",
"eslint-plugin-yml": "1.13.1",
"eslint-plugin-yml": "1.13.2",
"fs-extra": "11.2.0",
"husky": "9.0.11",
"lint-staged": "15.2.2",

815
pnpm-lock.yaml generated
View File

File diff suppressed because it is too large Load Diff