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

This commit is contained in:
DIYgod
2024-04-01 15:29:02 +08:00
204 changed files with 3452 additions and 1963 deletions

View File

@@ -2,4 +2,7 @@ coverage
.vscode
docker-compose.yml
!/.github
!/docs/.vuepress
lib/routes-deprecated
lib/router.js
babel.config.js
scripts/docker/minify-docker.js

2
.github/FUNDING.yml vendored
View File

@@ -2,4 +2,4 @@
github: DIYgod
patreon: DIYgod
open_collective: RSSHub
custom: ['https://afdian.net/a/diygod', 'https://docs.rsshub.app/support']
custom: ['https://afdian.net/a/diygod', 'https://docs.rsshub.app/sponsor']

View File

@@ -6,7 +6,7 @@ body:
- type: markdown
attributes:
value: |
Please ensure you have read [documentation](https://docs.rsshub.app/en), and provide all the information required by this template, otherwise the issue will be closed immediately.
Please ensure you have read [documentation](https://docs.rsshub.app/), and provide all the information required by this template, otherwise the issue will be closed immediately.
Due to the anti-crawling policy implemented by certain websites, some RSS routes provided by the demo will return status code 403. This is not an issue caused by RSSHub and please do not report it.
- type: textarea

View File

@@ -7,7 +7,7 @@ body:
- type: markdown
attributes:
value: |
Please ensure the feature requested is not listed in [documentation](https://docs.rsshub.app/en) or [issue](https://github.com/DIYgod/RSSHub/issues), and is not a [new RSS proposal](https://github.com/DIYgod/RSSHub/issues/new?assignees=&labels=RSS+proposal&template=rss_request_en.yml), and provide all the information required by this template.
Please ensure the feature requested is not listed in [documentation](https://docs.rsshub.app/) or [issue](https://github.com/DIYgod/RSSHub/issues), and is not a [new RSS proposal](https://github.com/DIYgod/RSSHub/issues/new?assignees=&labels=RSS+proposal&template=rss_request_en.yml), and provide all the information required by this template.
Otherwise the issue will be closed immediately.
- type: textarea

View File

@@ -7,7 +7,7 @@ body:
- type: markdown
attributes:
value: |
Please ensure the RSS proposal is not listed in [documentation](https://docs.rsshub.app/en) or [issue](https://github.com/DIYgod/RSSHub/issues), website doesn't provide this kind of RSS feed, and provide all the information required by this template.
Please ensure the RSS proposal is not listed in [documentation](https://docs.rsshub.app/) or [issue](https://github.com/DIYgod/RSSHub/issues), website doesn't provide this kind of RSS feed, and provide all the information required by this template.
Otherwise the issue will be closed immediately.
We are flooded with feature requests and short-handed, please try to make it yourself, the [guide](https://docs.rsshub.app/joinus) is a good place to start. Submit a pull request when done!

View File

@@ -11,8 +11,6 @@ updates:
ignore:
- dependency-name: jsrsasign
versions: ['>=11.0.0'] # no longer includes KJUR.crypto.Cipher for RSA
- dependency-name: unified
versions: ['>=10.0.0']
- package-ecosystem: 'github-actions'
directory: '/'

View File

@@ -15,7 +15,7 @@ jobs:
build:
runs-on: ubuntu-latest
name: Build assets
timeout-minutes: 5
timeout-minutes: 60
permissions:
contents: write
steps:
@@ -33,13 +33,7 @@ jobs:
- name: Install dependencies (yarn)
run: pnpm i
- name: Build assets
run: npm run build
- name: Commit files
run: |
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" -a --no-verify && git push "https://${GITHUB_ACTOR}:${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git" HEAD:master)
run: pnpm build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
@@ -47,6 +41,10 @@ jobs:
publish_dir: ./assets
user_name: 'github-actions[bot]'
user_email: '41898282+github-actions[bot]@users.noreply.github.com'
# prevent deleting build/test-full-routes.json which will break build:docs
keep_files: true
- name: Build docs
run: pnpm build:docs
- name: Checkout docs
uses: actions/checkout@v4
with:
@@ -57,6 +55,8 @@ jobs:
run: |
cp -r ./assets/build/docs/en/* ./rsshub-docs/src/routes
cp -r ./assets/build/docs/zh/* ./rsshub-docs/src/zh/routes
cp ./lib/types.ts ./rsshub-docs/.vitepress/theme/types.ts
cp ./scripts/workflow/data.ts ./rsshub-docs/.vitepress/config/data.ts
- name: Commit docs
run: |
cd rsshub-docs

View File

@@ -15,16 +15,16 @@ jobs:
- uses: pnpm/action-setup@v3
with:
version: 8
- uses: actions/setup-node@v4 # just need its cache
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies (pnpm) # needed since we need to parse markdown, so we also use got instead
- name: Install dependencies (pnpm) # import remark-parse and unified
run: pnpm i
- name: Generate feedback
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-issue/call-maintainer.js`)
return script({ github, context, core })
const { default: callMaintainer } = await import('${{ github.workspace }}/scripts/workflow/test-issue/call-maintainer.mjs')
await callMaintainer({ github, context, core })

View File

@@ -2,14 +2,14 @@ name: PR - route test
on:
workflow_run:
workflows: [ PR - Docker build test ] # open, reopen, synchronized, edited included
types: [ completed ]
workflows: [PR - Docker build test] # open, reopen, synchronized, edited included
types: [completed]
jobs:
testRoute:
name: Route test
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }} # skip if unsuccessful
if: ${{ github.event.workflow_run.conclusion == 'success' }} # skip if unsuccessful
steps:
- uses: actions/checkout@v4
@@ -44,8 +44,8 @@ jobs:
const body = PR.body
const number = PR.number
const sender = PR.user.login
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-route/identify.js`)
return script({ github, context, core }, body, number, sender)
const { default: identify } = await import('${{ github.workspace }}/scripts/workflow/test-route/identify.mjs')
return identify({ github, context, core }, body, number, sender)
- name: Fetch Docker image
if: (env.TEST_CONTINUE)
@@ -72,13 +72,13 @@ jobs:
with:
version: 8
- uses: actions/setup-node@v4 # just need its cache
- uses: actions/setup-node@v4
if: (env.TEST_CONTINUE)
with:
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies (pnpm) # `got` needed since `github.request` disallows HTTP requests
- name: Install dependencies (pnpm) # require js-beautify
if: (env.TEST_CONTINUE)
run: pnpm i
@@ -99,9 +99,8 @@ jobs:
const routes = JSON.parse(process.env.TEST_ROUTES)
const number = PR.number
core.info(`${link}, ${routes}, ${number}`)
const got = require("got")
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-route/test.js`)
return script({ github, context, core, got }, link, routes, number)
const { default: test } = await import('${{ github.workspace }}/scripts/workflow/test-route/test.mjs')
await test({ github, context, core }, link, routes, number)
- name: Pull Request Labeler
if: ${{ failure() }}
@@ -110,8 +109,8 @@ jobs:
actions: 'add-labels'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ steps.source-run-info.outputs.pullRequestNumber }}
labels: 'Route Test: Failed'
labels: 'Auto: Route Test Failed'
- name: Print Docker container logs
if: (env.TEST_CONTINUE)
run: docker logs rsshub # logs/combined.log? Not so readable...
run: docker logs rsshub # logs/combined.log? Not so readable...

View File

@@ -60,7 +60,7 @@ jobs:
actions: 'add-labels'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.pull_request.number }}
labels: 'Route Test: Failed'
labels: 'Auto: Route Test Failed'
- name: Test Docker image
run: bash scripts/docker/test-docker.sh

View File

@@ -77,8 +77,8 @@ jobs:
const body = event.comment.body
const number = event.issue.number
const sender = event.comment.user.login
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-route/identify.js`)
return script({ github, context, core }, body, number, sender)
const { default: identify } = await import('${{ github.workspace }}/scripts/workflow/test-route/identify.mjs')
return identify({ github, context, core }, body, number, sender)
- name: Build RSSHub
if: env.TEST_CONTINUE
@@ -107,9 +107,8 @@ jobs:
const routes = JSON.parse(process.env.TEST_ROUTES)
const number = event.issue.number
core.info(`${link}, ${routes}, ${number}`)
const got = require("got")
const script = require(`${process.env.GITHUB_WORKSPACE}/scripts/workflow/test-route/test.js`)
return script({ github, context, core, got }, link, routes, number)
const { default: test } = await import('${{ github.workspace }}/scripts/workflow/test-route/test.mjs')
await test({ github, context, core }, link, routes, number)
- name: Print logs
if: (env.TEST_CONTINUE)

44
.github/workflows/test-full-routes.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Build assets (Full Routes Test Result)
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
name: Build assets
timeout-minutes: 60
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: Use Node.js Active LTS
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies (yarn)
run: pnpm i
- name: Build assets
run: pnpm build
- name: Build full routes test result
continue-on-error: true
run: pnpm vitest:fullroutes
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./assets
user_name: 'github-actions[bot]'
user_email: '41898282+github-actions[bot]@users.noreply.github.com'
keep_files: true

View File

@@ -5,7 +5,6 @@ on:
branches-ignore:
- 'dependabot/**'
paths:
- 'test/**'
- 'lib/**'
- 'package.json'
- 'pnpm-lock.yaml'
@@ -43,6 +42,8 @@ jobs:
run: pnpm i
- name: Run postinstall script for dependencies
run: pnpm rb
- name: Build routes
run: pnpm build
- name: Test all and generate coverage
run: pnpm run vitest:coverage
env:
@@ -84,6 +85,8 @@ jobs:
run: pnpm i
- name: Run postinstall script for dependencies
run: pnpm rb
- name: Build routes
run: pnpm build
- name: Install Chromium
if: ${{ matrix.chromium.dependency != '' }}
# 'chromium-browser' from Ubuntu APT repo is a dummy package. Its version (85.0.4183.83) means

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
package-lock=true

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
lib/routes-deprecated
lib/router.js
babel.config.js
scripts/docker/minify-docker.js

View File

View File

@@ -1 +1 @@
## Please refer to [Join Us](https://docs.rsshub.app/joinus/quick-start)
## Please refer to [Join Us](https://docs.rsshub.app/joinus/)

View File

@@ -84,7 +84,7 @@ FROM node:21-bookworm-slim AS chromium-downloader
# Yeah, downloading Chromium never needs those dependencies below.
WORKDIR /app
COPY ./.puppeteerrc.js /app/
COPY ./.puppeteerrc.cjs /app/
COPY --from=dep-version-parser /ver/.puppeteer_version /app/.puppeteer_version
ARG TARGETPLATFORM

View File

@@ -31,7 +31,7 @@ RSSHub can be used with browser extension [RSSHub Radar](https://github.com/DIYg
<a href="https://rss3.io" target="_blank"><img height="50px" src="https://i.imgur.com/lb1dDGK.png"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://xlog.app/" target="_blank"><img height="50px" src="https://i.imgur.com/JuhHTKD.png"></a>
</p>
[![](https://opencollective.com/static/images/become_sponsor.svg)](https://docs.rsshub.app/support/)
[![](https://opencollective.com/static/images/become_sponsor.svg)](https://docs.rsshub.app/sponsor/)
### Contributors
@@ -54,15 +54,15 @@ Logo designer [sheldonrrr](https://dribbble.com/sheldonrrr)
We welcome all pull requests. Suggestions and feedback are also welcomed [here](https://github.com/DIYgod/RSSHub/issues).
Refer to [Join Us](https://docs.rsshub.app/joinus/quick-start)
Refer to [Join Us](https://docs.rsshub.app/joinus/)
## Deployment
Refer to [Deployment](https://docs.rsshub.app/install/)
Refer to [Deployment](https://docs.rsshub.app/deploy/)
## Support RSSHub
Refer to [Support RSSHub](https://docs.rsshub.app/support/)
Refer to [Support RSSHub](https://docs.rsshub.app/sponsor/)
RSSHub is open source and completely free under the MIT license. However, just like any other open source project, as the project grows, the hosting, development and maintenance requires funding support.

View File

@@ -12,6 +12,6 @@ const app = require('../lib/app');
const logger = require('../lib/utils/logger');
logger.info(`🎉 RSSHub is running! Cheers!`);
logger.info('💖 Can you help keep this open source project alive? Please sponsor 👉 https://docs.rsshub.app/support');
logger.info('💖 Can you help keep this open source project alive? Please sponsor 👉 https://docs.rsshub.app/sponsor');
module.exports = handle(app);

View File

@@ -1,4 +1,4 @@
import '@/utils/request-wrapper';
import '@/utils/request-rewriter';
import { Hono } from 'hono';

View File

@@ -1,5 +1,4 @@
import { describe, expect, it, afterEach, vi } from 'vitest';
import nock from 'nock';
afterEach(() => {
vi.resetModules();
@@ -93,11 +92,6 @@ describe('config', () => {
it('remote config', async () => {
process.env.REMOTE_CONFIG = 'http://rsshub.test/config';
nock(/rsshub\.test/)
.get('/config')
.reply(200, {
UA: 'test',
});
const { config } = await import('./config');
await new Promise((resolve) => setTimeout(resolve, 100));
expect(config.ua).toBe('test');

View File

@@ -1,6 +1,6 @@
import 'dotenv/config';
import randUserAgent from '@/utils/rand-user-agent';
import got from 'got';
import { ofetch } from 'ofetch';
let envs = process.env;
@@ -634,9 +634,8 @@ const calculateValue = () => {
calculateValue();
if (envs.REMOTE_CONFIG) {
got.get(envs.REMOTE_CONFIG)
.then(async (response) => {
const data = JSON.parse(response.body);
ofetch(envs.REMOTE_CONFIG)
.then(async (data) => {
if (data) {
envs = Object.assign(envs, data);
calculateValue();

View File

@@ -22,7 +22,7 @@ describe('httperror', () => {
it(`httperror`, async () => {
const response = await request.get('/test/httperror');
expect(response.status).toBe(503);
expect(response.text).toMatch('Response code 404 (Not Found): target website might be blocking our access, you can host your own RSSHub instance for a better usability.');
expect(response.text).toMatch('404 Not Found: target website might be blocking our access, you can host your own RSSHub instance for a better usability.');
}, 20000);
});

View File

@@ -39,7 +39,7 @@ export const errorHandler: ErrorHandler = (error, ctx) => {
}
let message = '';
if (error.name && (error.name === 'HTTPError' || error.name === 'RequestError')) {
if (error.name && (error.name === 'HTTPError' || error.name === 'RequestError' || error.name === 'FetchError')) {
ctx.status(503);
message = `${error.message}: target website might be blocking our access, you can host your own RSSHub instance for a better usability.`;
} else if (error instanceof RequestInProgressError) {

View File

@@ -8,7 +8,7 @@ const port = config.connect.port;
const hostIPList = getLocalhostAddress();
logger.info(`🎉 RSSHub is running on port ${port}! Cheers!`);
logger.info('💖 Can you help keep this open source project alive? Please sponsor 👉 https://docs.rsshub.app/support');
logger.info('💖 Can you help keep this open source project alive? Please sponsor 👉 https://docs.rsshub.app/sponsor');
logger.info(`🔗 Local: 👉 http://localhost:${port}`);
for (const ip of hostIPList) {
logger.info(`🔗 Network: 👉 http://${ip}:${port}`);

View File

@@ -1,23 +1,16 @@
import { describe, expect, it, vi, afterEach, afterAll, beforeAll } from 'vitest';
import { describe, expect, it, vi, afterEach } from 'vitest';
import Parser from 'rss-parser';
import wait from '@/utils/wait';
process.env.CACHE_EXPIRE = '1';
process.env.CACHE_CONTENT_EXPIRE = '2';
const parser = new Parser();
beforeAll(() => {
process.env.CACHE_EXPIRE = '1';
process.env.CACHE_CONTENT_EXPIRE = '3';
});
afterEach(() => {
delete process.env.CACHE_TYPE;
vi.resetModules();
});
afterAll(() => {
delete process.env.CACHE_EXPIRE;
});
describe('cache', () => {
it('memory', async () => {
process.env.CACHE_TYPE = 'memory';
@@ -45,7 +38,7 @@ describe('cache', () => {
expect(response3.headers).not.toHaveProperty('rsshub-cache-status');
const parsed3 = await parser.parseString(await response3.text());
await wait(3 * 1000 + 100);
await wait(2 * 1000 + 100);
const response4 = await app.request('/test/cache');
const parsed4 = await parser.parseString(await response4.text());
@@ -58,7 +51,7 @@ describe('cache', () => {
await wait(1 * 1000 + 100);
const response5 = await app.request('/test/refreshCache');
const parsed5 = await parser.parseString(await response5.text());
await wait(2 * 1000 + 100);
await wait(1 * 1000 + 100);
const response6 = await app.request('/test/refreshCache');
const parsed6 = await parser.parseString(await response6.text());
@@ -93,7 +86,7 @@ describe('cache', () => {
expect(response3.headers).not.toHaveProperty('rsshub-cache-status');
const parsed3 = await parser.parseString(await response3.text());
await wait(3 * 1000 + 100);
await wait(2 * 1000 + 100);
const response4 = await app.request('/test/cache');
const parsed4 = await parser.parseString(await response4.text());
@@ -106,7 +99,7 @@ describe('cache', () => {
await wait(1 * 1000 + 100);
const response5 = await app.request('/test/refreshCache');
const parsed5 = await parser.parseString(await response5.text());
await wait(2 * 1000 + 100);
await wait(1 * 1000 + 100);
const response6 = await app.request('/test/refreshCache');
const parsed6 = await parser.parseString(await response6.text());

View File

@@ -1,8 +1,12 @@
import { describe, expect, it, vi } from 'vitest';
import app from '@/app';
import Parser from 'rss-parser';
import { config } from '@/config';
import nock from 'nock';
process.env.OPENAI_API_KEY = 'sk-1234567890';
process.env.OPENAI_API_ENDPOINT = 'https://api.openai.mock/v1';
vi.mock('@/utils/request-rewriter', () => ({ default: null }));
const { config } = await import('@/config');
const { default: app } = await import('@/app');
const parser = new Parser();
@@ -424,26 +428,6 @@ describe('multi parameter', () => {
describe('openai', () => {
it(`chatgpt`, async () => {
vi.resetModules();
process.env.OPENAI_API_KEY = 'sk-1234567890';
const app = (await import('@/app')).default;
const { config } = await import('@/config');
nock(config.openai.endpoint)
.post('/chat/completions')
.reply(() => [
200,
{
choices: [
{
message: {
content: 'Summary of the article.',
},
},
],
},
]);
const responseWithGpt = await app.request('/test/gpt?chatgpt=true');
const responseNormal = await app.request('/test/gpt');

View File

@@ -1,7 +1,7 @@
import * as entities from 'entities';
import { load, type CheerioAPI, type Element } from 'cheerio';
import { simplecc } from 'simplecc-wasm';
import got from '@/utils/got';
import ofetch from '@/utils/ofetch';
import { config } from '@/config';
import { RE2JS } from 're2js';
import markdownit from 'markdown-it';
@@ -34,8 +34,9 @@ const resolveRelativeLink = ($: CheerioAPI, elem: Element, attr: string, baseUrl
const summarizeArticle = async (articleText: string) => {
const apiUrl = `${config.openai.endpoint}/chat/completions`;
const response = await got.post(apiUrl, {
json: {
const response = await ofetch(apiUrl, {
method: 'POST',
body: {
model: config.openai.model,
max_tokens: config.openai.maxTokens,
messages: [
@@ -49,8 +50,7 @@ const summarizeArticle = async (articleText: string) => {
},
});
// @ts-expect-error custom field
return response.data.choices[0].message.content;
return response.choices[0].message.content;
};
const getAuthorString = (item) => {
@@ -305,8 +305,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
if (link) {
// if parser failed, return default description and not report error
try {
// @ts-expect-error custom field
const { data: res } = await got(link);
const res = await ofetch(link);
const $ = load(res);
const result = await Parser.parse(link, {
html: $.html(),
@@ -380,7 +379,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
}
}
} else {
throw new Error(`Invalid parameter brief. Please check the doc https://docs.rsshub.app/parameter#shu-chu-jian-xun`);
throw new Error(`Invalid parameter brief. Please check the doc https://docs.rsshub.app/guide/parameters#shu-chu-jian-xun`);
}
}
// some parameters are processed in `anti-hotlink.js`

View File

@@ -1,55 +1,8 @@
import { describe, expect, it, afterAll } from 'vitest';
process.env.SOCKET = 'socket';
import { describe, expect, it } from 'vitest';
import app from '@/app';
import Parser from 'rss-parser';
const parser = new Parser();
import { config } from '@/config';
afterAll(() => {
delete process.env.SOCKET;
});
async function checkRSS(response) {
const checkDate = (date) => {
expect(date).toEqual(expect.any(String));
expect(Date.parse(date)).toEqual(expect.any(Number));
expect(Date.now() - +new Date(date)).toBeGreaterThan(-1000 * 60 * 60 * 24 * 5);
expect(Date.now() - +new Date(date)).toBeLessThan(1000 * 60 * 60 * 24 * 30 * 12 * 10);
};
const parsed = await parser.parseString(await response.text());
expect(parsed).toEqual(expect.any(Object));
expect(parsed.title).toEqual(expect.any(String));
expect(parsed.title).not.toBe('RSSHub');
expect(parsed.description).toEqual(expect.any(String));
expect(parsed.link).toEqual(expect.any(String));
expect(parsed.lastBuildDate).toEqual(expect.any(String));
expect(parsed.ttl).toEqual(Math.trunc(config.cache.routeExpire / 60) + '');
expect(parsed.items).toEqual(expect.any(Array));
checkDate(parsed.lastBuildDate);
// check items
const guids: (string | undefined)[] = [];
for (const item of parsed.items) {
expect(item).toEqual(expect.any(Object));
expect(item.title).toEqual(expect.any(String));
expect(item.link).toEqual(expect.any(String));
expect(item.content).toEqual(expect.any(String));
expect(item.guid).toEqual(expect.any(String));
if (item.pubDate) {
expect(item.pubDate).toEqual(expect.any(String));
checkDate(item.pubDate);
}
// guid must be unique
expect(guids).not.toContain(item.guid);
guids.push(item.guid);
}
}
describe('router', () => {
describe('registry', () => {
// root
it(`/`, async () => {
const response = await app.request('/');
@@ -58,14 +11,6 @@ describe('router', () => {
expect(response.headers.get('cache-control')).toBe('no-cache');
});
// route
it(`/test/1`, async () => {
const response = await app.request('/test/1');
expect(response.status).toBe(200);
await checkRSS(response);
});
// robots.txt
it('/robots.txt', async () => {
config.disallowRobot = false;

View File

@@ -7,8 +7,6 @@ import { serveStatic } from '@hono/node-server/serve-static';
import index from '@/routes/index';
import robotstxt from '@/routes/robots.txt.ts';
import { namespace as testNamespace } from './routes/test/namespace';
import { route as testRoute } from '@/routes/test/index';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -27,18 +25,9 @@ let namespaces: Record<
switch (process.env.NODE_ENV) {
case 'test':
modules = {
'/test/namespace.ts': {
namespace: testNamespace,
},
'/test/index.ts': {
route: testRoute,
},
};
break;
case 'production':
// eslint-disable-next-line n/no-unpublished-require
namespaces = require('../assets/build/routes.json');
// @ts-expect-error
namespaces = await import('../assets/build/routes.json');
break;
default:
modules = directoryImport({
@@ -56,7 +45,7 @@ if (Object.keys(modules).length) {
| {
namespace: Namespace;
};
const namespace = module.split('/')[1];
const namespace = module.split(/[/\\]/)[1];
if ('namespace' in content) {
namespaces[namespace] = Object.assign(
{
@@ -76,13 +65,13 @@ if (Object.keys(modules).length) {
for (const path of content.route.path) {
namespaces[namespace].routes[path] = {
...content.route,
location: module.split('/').slice(2).join('/'),
location: module.split(/[/\\]/).slice(2).join('/'),
};
}
} else {
namespaces[namespace].routes[content.route.path] = {
...content.route,
location: module.split('/').slice(2).join('/'),
location: module.split(/[/\\]/).slice(2).join('/'),
};
}
}

View File

@@ -379,9 +379,6 @@ router.get('/geekpark/breakingnews', lazyloadRouteHandler('./routes/geekpark/bre
// 香港天文台
router.get('/hko/weather', lazyloadRouteHandler('./routes/hko/weather'));
// 技术头条
router.get('/blogread/newest', lazyloadRouteHandler('./routes/blogread/newest'));
// gnn游戏新闻
router.get('/gnn/gnn', lazyloadRouteHandler('./routes/gnn/gnn'));

View File

@@ -1,24 +0,0 @@
const got = require('@/utils/got');
const host = 'http://www.005.tv/zx';
const cheerio = require('cheerio');
module.exports = async (ctx) => {
const response = await got(host);
const data = response.body;
const $ = cheerio.load(data);
const list = $('div.article-list li');
ctx.state.data = {
title: $('head > title').text(),
link: host,
description: '二次元资讯',
item: list
.map((index, item) => ({
title: $(item).find('h3 > a').text().trim(),
description: `<img src="${$(item).find('img').attr('src')}" /> <br>
${$(item).find('div.p-row').text()}`,
link: $(item).find('h3 > a').attr('href'),
pubDate: new Date($(item).find('span.fr.time').text().trim().slice(0, 4), $(item).find('span.fr.time').text().trim().slice(5, 7), $(item).find('span.fr.time').text().trim().slice(8, 12)).toUTCString(),
}))
.get(),
};
};

View File

@@ -1,69 +0,0 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const categories = {
latest: 'latest',
popular: 'popular',
published: 'published',
abstract: 'latest:15:',
action: 'latest:1:',
animals: 'latest:21:',
architecture: 'latest:11:',
conceptual: 'latest:17:',
'creative-edit': 'latest:10:',
documentary: 'latest:8:',
everyday: 'latest:14:',
'fine-art-nude': 'latest:12:',
humour: 'latest:3:',
landscape: 'latest:6:',
macro: 'latest:2:',
mood: 'latest:4:',
night: 'latest:9:',
performance: 'latest:19:',
portrait: 'latest:13:',
'still-life': 'latest:18:',
street: 'latest:7:',
underwater: 'latest:20:',
wildlife: 'latest:5:',
};
module.exports = async (ctx) => {
const category = ctx.params.category || 'latest';
const rootUrl = `https://1x.com`;
const currentUrl = `${rootUrl}/gallery/${category}`;
const apiUrl = `${rootUrl}/backend/lm.php?style=normal&mode=${categories[category]}&from=0&autoload=`;
const response = await got({
method: 'get',
url: apiUrl,
});
const $ = cheerio.load(response.data);
const items = $('root data')
.html()
.split('\n')
.slice(0, -1)
.map((item) => {
item = $(item);
const id = item
.find('.photos-feed-image')
.attr('id')
.match(/img-(\d+)/)[1];
return {
guid: id,
link: `${rootUrl}/photo/${id}`,
author: item.find('.photos-feed-data-name').eq(0).text(),
title: item.find('.photos-feed-data-title').text() || 'Untitled',
description: `<img src="${item.find('.photos-feed-image').attr('src')}">`,
};
});
ctx.state.data = {
title: `${category} - 1X`,
link: currentUrl,
item: items,
};
};

View File

@@ -3,7 +3,7 @@ const config = require('@/config').value;
module.exports = async (ctx) => {
if (!config.disqus || !config.disqus.api_key) {
throw new Error('Disqus RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('Disqus RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const forum = ctx.params.forum;

View File

@@ -3,7 +3,7 @@ const utils = require('./utils');
module.exports = async (ctx) => {
if (!config.fanfou || !config.fanfou.consumer_key || !config.fanfou.consumer_secret || !config.fanfou.username || !config.fanfou.password) {
throw new Error('Fanfou RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('Fanfou RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const uid = ctx.params.uid;

View File

@@ -3,7 +3,7 @@ const utils = require('./utils');
module.exports = async (ctx) => {
if (!config.fanfou || !config.fanfou.consumer_key || !config.fanfou.consumer_secret || !config.fanfou.username || !config.fanfou.password) {
throw new Error('Fanfou RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('Fanfou RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const fanfou = await utils.getFanfou();

View File

@@ -3,7 +3,7 @@ const utils = require('./utils');
module.exports = async (ctx) => {
if (!config.fanfou || !config.fanfou.consumer_key || !config.fanfou.consumer_secret || !config.fanfou.username || !config.fanfou.password) {
throw new Error('Fanfou RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('Fanfou RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const keyword = ctx.params.keyword;

View File

@@ -3,7 +3,7 @@ const utils = require('./utils');
module.exports = async (ctx) => {
if (!config.fanfou || !config.fanfou.consumer_key || !config.fanfou.consumer_secret || !config.fanfou.username || !config.fanfou.password) {
throw new Error('Fanfou RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('Fanfou RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const uid = ctx.params.uid;

View File

@@ -3,7 +3,7 @@ const config = require('@/config').value;
module.exports = async (ctx) => {
if (!config.lastfm || !config.lastfm.api_key) {
throw new Error('Last.fm RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('Last.fm RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const user = ctx.params.user;

View File

@@ -3,7 +3,7 @@ const config = require('@/config').value;
module.exports = async (ctx) => {
if (!config.lastfm || !config.lastfm.api_key) {
throw new Error('Last.fm RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('Last.fm RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const user = ctx.params.user;

View File

@@ -3,7 +3,7 @@ const config = require('@/config').value;
module.exports = async (ctx) => {
if (!config.lastfm || !config.lastfm.api_key) {
throw new Error('Last.fm RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('Last.fm RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const country = ctx.params.country;

78
lib/routes.test.ts Normal file
View File

@@ -0,0 +1,78 @@
import { describe, expect, it } from 'vitest';
import app from '@/app';
import Parser from 'rss-parser';
const parser = new Parser();
import { config } from '@/config';
process.env.ALLOW_USER_SUPPLY_UNSAFE_DOMAIN = 'true';
const routes = {
'/test/:id': '/test/1',
};
if (process.env.FULL_ROUTES_TEST) {
const { namespaces } = await import('@/registry');
for (const namespace in namespaces) {
for (const route in namespaces[namespace].routes) {
const requireConfig = namespaces[namespace].routes[route].features?.requireConfig;
let configs;
if (typeof requireConfig !== 'boolean') {
configs = requireConfig
?.filter((config) => !config.optional)
.map((config) => config.name)
.filter((name) => name !== 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN');
}
if (namespaces[namespace].routes[route].example && !configs?.length) {
routes[`/${namespace}${route}`] = namespaces[namespace].routes[route].example;
}
}
}
}
async function checkRSS(response) {
const checkDate = (date) => {
expect(date).toEqual(expect.any(String));
expect(Date.parse(date)).toEqual(expect.any(Number));
expect(Date.now() - +new Date(date)).toBeGreaterThan(-1000 * 60 * 60 * 24 * 5);
expect(Date.now() - +new Date(date)).toBeLessThan(1000 * 60 * 60 * 24 * 30 * 12 * 10);
};
const parsed = await parser.parseString(await response.text());
expect(parsed).toEqual(expect.any(Object));
expect(parsed.title).toEqual(expect.any(String));
expect(parsed.title).not.toBe('RSSHub');
expect(parsed.description).toEqual(expect.any(String));
expect(parsed.link).toEqual(expect.any(String));
expect(parsed.lastBuildDate).toEqual(expect.any(String));
expect(parsed.ttl).toEqual(Math.trunc(config.cache.routeExpire / 60) + '');
expect(parsed.items).toEqual(expect.any(Array));
checkDate(parsed.lastBuildDate);
// check items
const guids: (string | undefined)[] = [];
for (const item of parsed.items) {
expect(item).toEqual(expect.any(Object));
expect(item.title).toEqual(expect.any(String));
expect(item.link).toEqual(expect.any(String));
expect(item.content).toEqual(expect.any(String));
expect(item.guid).toEqual(expect.any(String));
if (item.pubDate) {
expect(item.pubDate).toEqual(expect.any(String));
checkDate(item.pubDate);
}
// guid must be unique
expect(guids).not.toContain(item.guid);
guids.push(item.guid);
}
}
describe('routes', () => {
for (const route in routes) {
it.concurrent(route, async () => {
const response = await app.request(routes[route]);
expect(response.status).toBe(200);
await checkRSS(response);
});
}
});

156
lib/routes/005/index.ts Normal file
View File

@@ -0,0 +1,156 @@
import { Route } from '@/types';
import { getCurrentPath } from '@/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
import timezone from '@/utils/timezone';
import { parseDate } from '@/utils/parse-date';
import { art } from '@/utils/render';
import * as path from 'node:path';
export const handler = async (ctx) => {
const { category = 'zx' } = ctx.req.param();
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
const rootUrl = 'https://005.tv';
const currentUrl = new URL(category ? `${category}/` : '', rootUrl).href;
const { data: response } = await got(currentUrl);
const $ = load(response);
const language = $('html').prop('lang');
let items = $('div.article-list ul li')
.slice(0, limit)
.toArray()
.map((item) => {
item = $(item);
const title = item.find('h3').text();
const image = item.find('img').prop('src');
const description = art(path.join(__dirname, 'templates/description.art'), {
intro: item.find('div.p-row').text(),
images: image
? [
{
src: image,
alt: title,
},
]
: undefined,
});
return {
title,
description,
pubDate: parseDate(item.find('span.time').text()),
link: new URL(item.find('h3 a').prop('href'), rootUrl).href,
content: {
html: description,
text: item.find('div.p-row').text(),
},
image,
banner: image,
language,
};
});
items = await Promise.all(
items.map((item) =>
cache.tryGet(item.link, async () => {
const { data: detailResponse } = await got(item.link);
const $$ = load(detailResponse);
const title = $$('h1.articleTitle-name').text();
const description = $$('div.articleContent').html();
item.title = title;
item.description = description;
item.pubDate = timezone(parseDate($$('.time').text()), +8);
item.category = $$('meta[name="keywords"]').prop('content').split(/,/);
item.content = {
html: description,
text: $$('div.articleContent').text(),
};
item.language = language;
return item;
})
)
);
const title = $('title').text();
const image = new URL('templets/muban/style/images/logo.png', rootUrl).href;
return {
title,
description: title.split(/_/)[0],
link: currentUrl,
item: items,
allowEmpty: true,
image,
author: title.split(/,/).pop(),
language,
};
};
export const route: Route = {
path: '/:category?',
name: '资讯',
url: '005.tv',
maintainers: ['nczitzk'],
handler,
example: '/005/zx',
parameters: { category: '分类,可在对应分类页 URL 中找到,默认为二次元资讯' },
description: `
| 二次元资讯 | 慢慢说 | 道听途说 | 展会资讯 |
| ---------- | ------ | -------- | -------- |
| zx | zwh | dtts | zh |
`,
categories: ['anime'],
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportRadar: true,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['005.tv/:category'],
target: (params) => {
const category = params.category;
return `/005${category ? `/${category}` : ''}`;
},
},
{
title: '二次元资讯',
source: ['005.tv/zx/'],
target: '/005/zx',
},
{
title: '慢慢说',
source: ['005.tv/zwh/'],
target: '/005/zwh',
},
{
title: '道听途说',
source: ['005.tv/dtts/'],
target: '/005/dtts',
},
{
title: '展会资讯',
source: ['005.tv/zh/'],
target: '/005/zh',
},
],
};

View File

@@ -0,0 +1,8 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '幻之羁绊动漫网',
url: '005.tv',
categories: ['anime'],
description: '',
};

View File

@@ -0,0 +1,27 @@
{{ if images }}
{{ each images image }}
{{ if !videos?.[0]?.src && image?.src }}
<figure>
<img
{{ if image.alt }}
alt="{{ image.alt }}"
{{ /if }}
{{ if image.width }}
alt="{{ image.width }}"
{{ /if }}
{{ if image.height }}
alt="{{ image.height }}"
{{ /if }}
src="{{ image.src }}">
</figure>
{{ /if }}
{{ /each }}
{{ /if }}
{{ if intro }}
<blockquote>{{ intro }}</blockquote>
{{ /if }}
{{ if description }}
{{@ description }}
{{ /if }}

View File

@@ -11,7 +11,7 @@ export const route: Route = {
path: '/dy2/:id',
categories: ['new-media'],
example: '/163/dy2/T1555591616739',
parameters: { id: 'id该网易号主页网址最后一项html的文件名' },
parameters: { id: 'id该网易号主页网址最后一项 html 的文件名' },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -21,7 +21,7 @@ export const route: Route = {
supportScihub: false,
},
name: '网易号(通用)',
maintainers: ['mjysci'],
maintainers: ['mjysci', 'lyqluis'],
handler,
description: `优先使用方法一,若是网易号搜索页面搜不到的小众网易号(文章页面不含\`data-wemediaid\`)则可使用此法。
触发反爬会只抓取到标题,建议自建。`,
@@ -43,11 +43,12 @@ async function handler(ctx) {
.toArray()
.map((item) => {
item = $(item);
const itemImg = item.find('a.img img');
return {
title: item.find('h4 a').text(),
link: item.find('a').first().attr('href'),
pubDate: timezone(parseDate(item.find('.time').text()), 8),
imgsrc: item.find('a img').attr('src'),
imgsrc: itemImg.attr('src') ?? itemImg.attr('_src'),
};
});

View File

@@ -27,7 +27,7 @@ export const route: Route = {
async function handler(ctx) {
if (!config.ncm || !config.ncm.cookies) {
throw new Error('163 Music RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('163 Music RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
}
const id = ctx.req.param('id');

View File

@@ -26,8 +26,9 @@ const parseDyArticle = (charset, item, tryGet) =>
}
});
const imgUrl = new URL(item.imgsrc);
item.description = art(path.join(__dirname, 'templates/dy.art'), {
imgsrc: item.imgsrc?.split('?')[0],
imgsrc: imgUrl.searchParams.get('url'),
postBody: $('.post_body').html(),
});

View File

@@ -1,6 +1,11 @@
{{ if cover }}
<img src="{{ cover }}">
{{ /if }}
<p>
{{each category}}
<code>{{ $value }}</code>
{{/each}}
</p>
<p>{{ introduction }}</p>
{{ each images image }}
<img src="{{ image }}">

View File

@@ -67,6 +67,7 @@ const ProcessItems = async (ctx, currentUrl, rootUrl) => {
.toArray()
.map((image) => content(image).attr('data-original')),
cover: content('.thumb-overlay img').first().attr('src'),
category: item.category,
});
return item;

View File

@@ -1,88 +1,147 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
import timezone from '@/utils/timezone';
import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/:path?',
categories: ['multimedia'],
example: '/1lou/search-繁花.htm',
parameters: { path: '路径信息在URL里找到,主页为 index' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['1lou.me/:path'],
target: '/:path',
},
],
name: '搜索',
maintainers: ['falling'],
handler,
description: `:::tip
将 1lou.me/ 后的内容作为参数传入到 path 即可
const rootUrl = 'https://www.1lou.me';
[www.1lou.me/search - 繁花.htm](http://www.1lou.me/search-繁花.htm) --> /1lou/search - 繁花.htm
export const handler = async (ctx) => {
const { params } = ctx.req.param();
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 50;
[www.1lou.me/forum-1.htm](http://www.1lou.me/forum-1.htm) --> /1lou/forum-1.htm
const queryString = Object.entries(ctx.req.query())
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
[www.1lou.me/](http://www.1lou.me/) --> /1lou/
:::`,
};
const currentUrl = new URL(`${params && params.endsWith('.htm') ? params : `${params}.htm`}${queryString ? `?${queryString}` : ''}`, rootUrl).href;
async function handler(ctx) {
const path = ctx.req.param('path') ?? '';
const rootUrl = `https://www.1lou.me`;
const currentUrl = `${rootUrl}/${path}`;
const response = await got({
method: 'get',
url: currentUrl,
});
const $ = load(response.data);
const { data: response } = await got(currentUrl);
let items = $('li.media.thread.tap:not(.hidden-sm)')
const $ = load(response);
const language = $('html').prop('lang');
let items = $('li.media.thread.tap:not(li.hidden-sm)')
.slice(0, limit)
.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();
item = $(item);
const subjectEl = item.find('div.subject').children('a').first();
return {
title: title.text(),
link: `${rootUrl}/${title.attr('href')}`,
author,
pubDate: timezone(parseDate(pubDate), +8),
title: subjectEl.text(),
pubDate: timezone(parseDate(item.find('span.date').text()), +8),
link: new URL(subjectEl.prop('href'), rootUrl).href,
category: [
item.find('a.text-secondary').text().replaceAll('[]', ''),
...item
.find('a.badge')
.toArray()
.map((c) => $(c).text()),
].filter(Boolean),
author: item.find('a.username').text(),
language,
};
});
items = await Promise.all(
items.map((item) =>
cache.tryGet(item.link, async () => {
const detailResponse = await got({
method: 'get',
url: item.link,
});
const content = 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')}`;
const { data: detailResponse } = await got(item.link);
const $$ = load(detailResponse);
const title = $$('h4.break-all').contents().last().text();
if (title) {
const description = $$('div.message.break-all').html();
const image = new URL($$('img').first().prop('src'), rootUrl).href;
item.title = title;
item.description = description;
item.pubDate = timezone(parseDate($$('span.date').text()), +8);
item.category = $$('a.badge')
.toArray()
.map((c) => $$(c).text());
item.content = {
html: description,
text: $$('div.message.break-all').text(),
};
item.image = image;
item.banner = image;
item.language = language;
const torrents = $$('ul.attachlist li a');
if (torrents.length > 0) {
const torrent = torrents.first();
item.enclosure_url = new URL(torrent.prop('href'), rootUrl).href;
item.enclosure_type = 'application/x-bittorrent';
item.enclosure_title = torrent.text();
}
}
return item;
})
)
);
const author = 'BT 之家 1LOU 站';
const image = new URL($('img.logo-2').prop('src'), rootUrl).href;
return {
title: '1Lou',
title: `${$('title').text().split(/-/)[0]} - ${author}`,
description: $('meta[name="description"]').prop('content'),
link: currentUrl,
item: items,
allowEmpty: true,
image,
author,
language,
};
}
};
export const route: Route = {
path: '/:params{.+}?',
name: '通用',
url: '1lou.me',
maintainers: ['falling', 'nczitzk'],
handler,
example: '/1lou/forum-2-1',
parameters: { params: '路径参数,可以在对应页面的 URL 中找到' },
description: `:::tip
\`1lou.me/\` 后的内容填入 params 参数,以下是几个例子:
若订阅 [大陆电视剧](https://www.1lou.me/forum-2-1.htm?tagids=0_97_0_0),网址为 \`https://www.1lou.me/forum-2-1.htm?tagids=0_97_0_0\`。截取 \`https://www.1lou.me/\` 到末尾 \`.htm\` 的部分 \`forum-2-1\` 作为参数,并补充 \`tagids\`,此时路由为 [\`/1lou/forum-2-1?tagids=0_97_0_0\`](https://rsshub.app/1lou/forum-2-1?tagids=0_97_0_0)。
若订阅 [最新发帖电视剧](https://www.1lou.me/forum-2-1.htm?orderby=tid&digest=0),网址为 \`https://www.1lou.me/forum-2-1.htm?orderby=tid&digest=0\`。截取 \`https://www.1lou.me/\` 到末尾 \`.htm\` 的部分 \`forum-2-1\` 作为参数,并补充 \`orderby\`,此时路由为 [\`/1lou/forum-2-1?orderby=tid\`](https://rsshub.app/1lou/forum-2-1?orderby=tid)。
若订阅 [搜素繁花主题贴](https://www.1lou.me/search-_E7_B9_81_E8_8A_B1-1.htm),网址为 \`https://www.1lou.me/search-_E7_B9_81_E8_8A_B1-1.htm\`。截取 \`https://www.1lou.me/\` 到末尾 \`.htm\` 的部分 \`search-_E7_B9_81_E8_8A_B1-1\` 作为参数,此时路由为 [\`/1lou/search-_E7_B9_81_E8_8A_B1-1\`](https://rsshub.app/1lou/search-_E7_B9_81_E8_8A_B1-1)。
:::`,
categories: ['multimedia'],
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportRadar: true,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['1lou.me/:params'],
target: (_, url) => {
url = new URL(url);
return `/1lou${url.href.replace(rootUrl, '')}`;
},
},
],
};

View File

@@ -3,4 +3,6 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'BT 之家 1LOU 站',
url: '1lou.me',
categories: ['multimedia'],
description: '',
};

121
lib/routes/1x/index.ts Normal file
View File

@@ -0,0 +1,121 @@
import { Route } from '@/types';
import { getCurrentPath } from '@/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);
import got from '@/utils/got';
import { load } from 'cheerio';
import { art } from '@/utils/render';
import * as path from 'node:path';
export const handler = async (ctx) => {
const { category = 'latest/awarded' } = ctx.req.param();
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30;
const rootUrl = 'https://1x.com';
const currentUrl = new URL(`gallery/${category}`, rootUrl).href;
const { data: currentResponse } = await got(currentUrl);
const $ = load(currentResponse);
const language = $('html').prop('lang');
const apiUrl = new URL(`backend/lm2.php?style=normal&mode=${$('input#lm_mode').prop('value')}`, rootUrl).href;
const { data: response } = await got(apiUrl);
const $$ = load(response);
const items = $$('div.photos-feed-item')
.slice(0, limit)
.toArray()
.map((item) => {
item = $(item);
const title = item.find('span.photos-feed-data-title').first().text() || 'Untitled';
const image = item.find('img').prop('src');
const author = item.find('span.photos-feed-data-name').first().text();
const text = `${title} by ${author}`;
const description = art(path.join(__dirname, 'templates/description.art'), {
images: image
? [
{
src: image,
alt: title,
},
]
: undefined,
description: text,
});
const id = item.find('img[id]').prop('id').split(/-/).pop();
const guid = `1x-${id}`;
return {
title,
description,
link: new URL(`photo/${id}`, rootUrl).href,
author,
guid,
id: guid,
content: {
html: description,
text,
},
image,
banner: image,
language,
enclosure_url: image,
enclosure_type: image ? `image/${image.split(/\./).pop()}` : undefined,
enclosure_title: title,
};
});
const image = new URL($('img.themedlogo').prop('src'), rootUrl).href;
return {
title: $('title').text(),
description: $('meta[name="description"]').prop('content'),
link: currentUrl,
item: items,
allowEmpty: true,
image,
author: $('meta[property="og:site_name"]').prop('content'),
language,
};
};
export const route: Route = {
path: '/:category{.+}?',
name: 'Gallery',
url: '1x.com',
maintainers: ['nczitzk'],
handler,
example: '/1x/latest/awarded',
parameters: { category: 'Category, Latest Awarded by default' },
description: `::: tip
Fill in the field in the path with the part of the corresponding page URL after \`https://1x.com/gallery/\` or \`https://1x.com/photo/\`. Here are the examples:
If you subscribe to [Abstract Awarded](https://1x.com/gallery/abstract/awarded), you should fill in the path with the part \`abstract/awarded\` from the page URL \`https://1x.com/gallery/abstract/awarded\`. In this case, the route will be [\`/1x/abstract/awarded\`](https://rsshub.app/1x/abstract/awarded).
If you subscribe to [Wildlife Published](https://1x.com/gallery/wildlife/published), you should fill in the path with the part \`wildlife/published\` from the page URL \`https://1x.com/gallery/wildlife/published\`. In this case, the route will be [\`/1x/wildlife/published\`](https://rsshub.app/1x/wildlife/published).
:::`,
categories: ['design', 'picture'],
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportRadar: true,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['/gallery/:category*', '/photos/:category*'],
target: '/1x/:category',
},
],
};

View File

@@ -0,0 +1,8 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '1x.com',
url: '1x.com',
categories: ['design', 'picture'],
description: '1x.com • In Pursuit of the Sublime. Browse 200,000 curated photos from photographers all over the world.',
};

View File

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

View File

@@ -31,9 +31,9 @@ export const route: Route = {
| 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> {
const category = (ctx!.req.param('category') ?? 'sgame').replaceAll('.', '/');
const tab = ctx?.req.param('tab') ?? 'all';
async function handler(ctx: Context): Promise<Data> {
const category = (ctx.req.param('category') ?? 'sgame').replaceAll('.', '/');
const tab = ctx.req.param('tab') ?? 'all';
const currentUrl = `https://www.2023game.com/${category}/`;

121
lib/routes/3kns/index.ts Normal file
View File

@@ -0,0 +1,121 @@
import { Data, DataItem, Route } from '@/types';
import got from '@/utils/got';
import { getCurrentPath } from '@/utils/helpers';
import { parseDate } from '@/utils/parse-date';
import { art } from '@/utils/render';
import { load } from 'cheerio';
import { Context } from 'hono';
import * as path from 'node:path';
const __dirname = getCurrentPath(import.meta.url);
export const route: Route = {
path: '/:filters?/:order?',
categories: ['game'],
example: '/3kns/category=all&lang=all',
parameters: {
filters: '过滤器,可用参数见下表',
order: '排序,按高分排序:desc;按低分排序:asc',
},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '3k-Switch游戏库',
maintainers: ['xzzpig'],
handler,
url: 'www.3kns.com/',
description: `游戏类型(category)
| 不限 | 角色扮演 | 动作冒险 | 策略游戏 | 模拟经营 | 即时战略 | 格斗类 | 射击游戏 | 休闲益智 | 体育运动 | 街机格斗 | 无双类 | 其他游戏 | 赛车竞速 |
| ---- | -------- | -------- | -------- | -------- | -------- | ------ | -------- | -------- | -------- | -------- | ------ | -------- | -------- |
| all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
游戏语言(language)
| 不限 | 中文 | 英语 | 日语 | 其他 | 中文汉化 | 德语 |
| ---- | ---- | ---- | ---- | ---- | -------- | ---- |
| all | 1 | 2 | 3 | 4 | 5 | 6 |
游戏标签(tag)
| 不限 | 热门 | 多人聚会 | 僵尸 | 体感 | 大作 | 音乐 | 三国 | RPG | 格斗 | 闯关 | 横版 | 科幻 | 棋牌 | 运输 | 无双 | 卡通动漫 | 日系 | 养成 | 恐怖 | 运动 | 乙女 | 街机 | 飞行模拟 | 解谜 | 海战 | 战争 | 跑酷 | 即时策略 | 射击 | 经营 | 益智 | 沙盒 | 模拟 | 冒险 | 竞速 | 休闲 | 动作 | 生存 | 独立 | 拼图 | 魔改 xci | 卡牌 | 塔防 |
| ---- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | --- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | -------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |
| all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
发售时间(pubDate)
| 不限 | 2017 年 | 2018 年 | 2019 年 | 2020 年 | 2021 年 | 2022 年 | 2023 年 | 2024 年 |
| ---- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- |
| all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
游戏集合(collection)
| 不限 | 舞力全开 | 马里奥 | 生化危机 | 炼金工房 | 最终幻想 | 塞尔达 | 宝可梦 | 勇者斗恶龙 | 模拟器 | 秋之回忆 | 第一方 | 体感健身 | 开放世界 | 儿童乐园 |
| ---- | -------- | ------ | -------- | -------- | -------- | ------ | ------ | ---------- | ------ | -------- | ------ | -------- | -------- | -------- |
| all | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |`,
};
async function handler(ctx: Context): Promise<Data> {
const filters = new URLSearchParams(ctx.req.param('filters'));
const order = ctx.req.param('order');
const category = filters.get('category') ?? 'all';
const language = filters.get('language') ?? 'all';
const tag = filters.get('tag') ?? 'all';
const pubDate = filters.get('pubDate') ?? 'all';
const collection = filters.get('collection') ?? 'all';
const baseUrl = 'https://www.3kns.com/';
const currentUrl = new URL(`${baseUrl}forum.php?mod=forumdisplay&fid=2&filter=sortid&typeid=0&sortid=1&searchsort=1&orderbystr=0`);
currentUrl.searchParams.set('dztgeshi', category);
currentUrl.searchParams.set('dztfenlei', language);
currentUrl.searchParams.set('nex_sg_tags', tag);
currentUrl.searchParams.set('deanbgbs', pubDate);
currentUrl.searchParams.set('nex_sg_stars', collection);
if (order !== undefined) {
currentUrl.searchParams.set('ascdescstr', order);
currentUrl.searchParams.set('orderbystr', 'nex_sg_score');
}
const response = await got(currentUrl);
const $ = load(response.data as any);
const selector = `form .newItem`;
const items: DataItem[] = $(selector)
.toArray()
.map((item) => {
const $item = $(item);
const title = $item.find('.showname a').text().trim();
const category = $item.find('.showtype').text().trim();
const pubDate = ($item.find('.showdate').contents()[0] as any).data.trim();
return {
title,
link: baseUrl + $item.find('.entry-media a').attr('href')!,
pubDate: parseDate(pubDate ?? ''),
category: [category],
description:
art(path.join(__dirname, 'templates/description.art'), {
cover: $item.find('.entry-media img').attr('src')?.trim().replace('.', baseUrl),
title,
tid: $item.find('.jb-chakan').text().trim(),
category,
language: $item.find('.jb-new').text().trim(),
pubDate,
system: $item.find('.jb-youxxx').text().trim(),
score: $item.find('.shownamep').text().trim(),
version: $item.find('.jb-youxbb').text().trim(),
}) ?? '',
};
});
return {
title: $('title').text(),
link: currentUrl.toString(),
allowEmpty: true,
item: items,
};
}

View File

@@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '3k-Switch游戏库',
url: 'www.3kns.com',
};

View File

@@ -0,0 +1,9 @@
<img src="{{ cover }}">
<h1>{{ title }}</h1>
<p>游戏TID{{ tid }}</p>
<p>类型:{{ category }}</p>
<p>语言:{{ language }}</p>
<p>更新日期:{{ pubDate }}</p>
<p>系统要求:{{ system }}</p>
<p>{{ score }}</p>
<p>游戏版本:{{ version }}</p>

View File

@@ -22,13 +22,14 @@ export const route: Route = {
},
name: 'Channel & Topic',
categories: ['traditional-media'],
description: `:::tip
All Topics in [Topic Library](https://abc.net.au/news/topics) are supported, you can fill in the field after \`topic\` in its URL, or fill in the \`documentId\`.
description: `
:::tip
All Topics in [Topic Library](https://abc.net.au/news/topics) are supported, you can fill in the field after \`topic\` in its URL, or fill in the \`documentId\`.
For example, the URL for [Computer Science](https://www.abc.net.au/news/topic/computer-science) is \`https://www.abc.net.au/news/topic/computer-science\`, the \`category\` is \`news/topic/computer-science\`, and the \`documentId\` of the Topic is \`2302\`, so the route is [/abc/news/topic/computer-science](https://rsshub.app/abc/news/topic/computer-science) and [/abc/2302](https://rsshub.app/abc/2302).
For example, the URL for [Computer Science](https://www.abc.net.au/news/topic/computer-science) is \`https://www.abc.net.au/news/topic/computer-science\`, the \`category\` is \`news/topic/computer-science\`, and the \`documentId\` of the Topic is \`2302\`, so the route is [/abc/news/topic/computer-science](https://rsshub.app/abc/news/topic/computer-science) and [/abc/2302](https://rsshub.app/abc/2302).
The supported channels are all listed in the table below. For other channels, please find the \`documentId\` in the source code of the channel page and fill it in as above.
:::`,
The supported channels are all listed in the table below. For other channels, please find the \`documentId\` in the source code of the channel page and fill it in as above.
:::`,
maintainers: ['nczitzk'],
handler,
};

View File

@@ -38,7 +38,6 @@ async function handler(ctx) {
title: item.node.title,
author: item.node.authors.map((author) => author.displayName).join(', '),
link: `https://aeon.co/${item.node.type.toLowerCase()}s/${item.node.slug}`,
pubDate: item.node.createdAt,
}));
const items = await getData(ctx, list);

View File

@@ -45,7 +45,6 @@ async function handler(ctx) {
const list = data.props.pageProps.articles.map((item) => ({
title: item.title,
link: `https://aeon.co/${binaryType}/${item.slug}`,
pubDate: item.createdAt,
}));
const items = await getData(ctx, list);

View File

@@ -16,6 +16,9 @@ const getData = async (ctx, list) => {
const data = JSON.parse($('script#__NEXT_DATA__').text());
const type = data.props.pageProps.article.type.toLowerCase();
item.pubDate = new Date(data.props.pageProps.article.publishedAt).toUTCString();
if (type === 'video') {
item.description = art(path.join(__dirname, 'templates/video.art'), { article: data.props.pageProps.article });
} else {
@@ -33,7 +36,7 @@ const getData = async (ctx, list) => {
const article = data.props.pageProps.article;
const capture = load(article.body);
const banner = article.thumbnail?.urls?.header;
const banner = article.image?.url;
capture('p.pullquote').remove();
const authorsBio = article.authors.map((author) => '<p>' + author.name + author.authorBio.replaceAll(/^<p>/g, ' ')).join('');

View File

@@ -1,4 +1,5 @@
import got from '@/utils/got';
import type { Route } from '@/types';
export const route: Route = {
path: '/dynamic/:uid?',

View File

@@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'AppsTorrent',
url: 'appstorrent.ru',
};

View File

@@ -0,0 +1,91 @@
import { Data, DataItem, Route } from '@/types';
import cache from '@/utils/cache';
import got, { Options } from '@/utils/got';
import { getCurrentPath } from '@/utils/helpers';
import { parseDate } from '@/utils/parse-date';
import { art } from '@/utils/render';
import { load } from 'cheerio';
import dayjs from 'dayjs';
import { Context } from 'hono';
import * as path from 'node:path';
const __dirname = getCurrentPath(import.meta.url);
export const route: Route = {
path: '/programs',
categories: ['program-update'],
example: '/appstorrent/programs',
name: 'Programs',
maintainers: ['xzzpig'],
handler,
url: 'appstorrent.ru/programs/',
};
async function handler(ctx?: Context): Promise<Data> {
const limit = ctx?.req.query('limit') ? Number.parseInt(ctx?.req.query('limit') ?? '20') : 20;
const baseUrl = 'https://appstorrent.ru';
const currentUrl = `${baseUrl}/programs/`;
const gotOptions: Options = {
http2: true,
};
const response = await got(currentUrl, gotOptions);
const $ = load(response.data as any);
const selector = 'article.soft-item:not(.locked)';
const list = $(selector)
.slice(0, limit)
.toArray()
.map((item) => {
const $item = $(item);
return {
title: $item.find('.subtitle').text().trim(),
link: $item.find('.subtitle a').attr('href')!,
category: [$item.find('.info .category').text().trim()],
version: $item.find('.version').text(),
architecture: $item.find('.architecture').text().trim(),
size: $item.find('.size').text().trim(),
};
});
const items: DataItem[] = await Promise.all(
list.map(
(item) =>
cache.tryGet(item.link, async () => {
const response = await got(item.link, gotOptions);
const $ = load(response.data as any);
const pubDate = parseDate($('.tech-info .date-news a').attr('href')?.replace('https://appstorrent.ru/', '') ?? '');
return {
title: item.title,
link: item.link,
category: item.category,
pubDate,
description: art(path.join(__dirname, 'templates/description.art'), {
cover: baseUrl + $('.main-title img').attr('src')?.trim(),
title: item.title,
pubDate: dayjs(pubDate).format('YYYY-MM-DD'),
version: item.version,
architecture: item.architecture,
compatibility: $('div.right > div.info > div.right-container > div:nth-child(5) > div > span:nth-child(2) > a').text(),
size: item.size,
activation: $('div.right > div.info > div.right-container > div:nth-child(4) > div > span:nth-child(2) > a').text(),
description: $('.content .body-content').first().text(),
changelog: $('.content .body-content').last().text(),
screenshots: $('.screenshots img')
.toArray()
.map((img) => $(img).attr('src'))
.map((src) => baseUrl + src),
}),
} as DataItem;
}) as Promise<DataItem>
)
);
return {
title: $('title').text(),
link: currentUrl.toString(),
allowEmpty: true,
item: items,
};
}

View File

@@ -0,0 +1,22 @@
<p>
<img src="{{ cover }}">
<h1>{{ title }}</h1><br>
<b>Public Date</b>: {{pubDate}}<br>
<b>Version</b>: {{version}}<br>
<b>Architecture</b>: {{architecture}}<br>
<b>Compactibility</b>: {{compatibility}}<br>
<b>Size</b>: {{size}}<br>
<b>Activation</b>: {{activation}}<br>
</p>
<b>Description</b>:
<p>
{{ description }}
</p>
<b>Change Log</b>:
<p>
{{ changelog }}
</p>
<b>Screenshots</b>
{{each screenshots}}
<img src="{{ $value }}">
{{/each}}

View File

@@ -117,11 +117,10 @@ async function handler(ctx) {
const response = await got({
method: 'get',
url: `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history?host_uid=${uid}`,
url: `https://api.vc.bilibili.com/dynamic_svr/v2/dynamic_svr/space_history?host_uid=${uid}`,
headers: {
Referer: `https://space.bilibili.com/${uid}/`,
},
transformResponse: [(data) => data],
});
const cards = JSONbig.parse(response.body).data.cards;

View File

@@ -45,7 +45,7 @@ async function handler(ctx) {
const cookie = config.bilibili.cookies[loginUid];
if (cookie === undefined) {
throw new Error('缺少对应 loginUid 的 Bilibili 用户登录后的 Cookie 值 <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">bilibili 用户关注动态系列路由</a>');
throw new Error('缺少对应 loginUid 的 Bilibili 用户登录后的 Cookie 值 <a href="https://docs.rsshub.app/zh/deploy/config#route-specific-configurations">bilibili 用户关注动态系列路由</a>');
}
const name = await cache.getUsernameFromUID(uid);

View File

@@ -43,7 +43,7 @@ async function handler(ctx) {
const loginUid = ctx.req.param('loginUid');
const cookie = config.bilibili.cookies[loginUid];
if (cookie === undefined) {
throw new Error('缺少对应 loginUid 的 Bilibili 用户登录后的 Cookie 值 <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">bilibili 用户关注动态系列路由</a>');
throw new Error('缺少对应 loginUid 的 Bilibili 用户登录后的 Cookie 值 <a href="https://docs.rsshub.app/zh/deploy/config#route-specific-configurations">bilibili 用户关注动态系列路由</a>');
}
const uid = ctx.req.param('uid');

View File

@@ -3,6 +3,7 @@ import got from '@/utils/got';
import cache from './cache';
import utils from './utils';
import { parseDate } from '@/utils/parse-date';
import { queryToBoolean } from '@/utils/readable-social';
const notFoundData = {
title: '此 bilibili 频道不存在',
@@ -15,7 +16,7 @@ export const route: Route = {
parameters: {
uid: '用户 id, 可在 UP 主主页中找到',
sid: '合集 id, 可在合集页面的 URL 中找到',
disableEmbed: '默认为开启内嵌视频, 任意值为关闭',
disableEmbed: '空,0与false为开启内嵌视频, 其他任意值为关闭',
sortReverse: '默认:默认排序 1:升序排序',
page: '页码, 默认1',
},
@@ -35,7 +36,7 @@ export const route: Route = {
async function handler(ctx) {
const uid = Number.parseInt(ctx.req.param('uid'));
const sid = Number.parseInt(ctx.req.param('sid'));
const disableEmbed = ctx.req.param('disableEmbed');
const disableEmbed = queryToBoolean(ctx.req.param('disableEmbed'));
const sortReverse = Number.parseInt(ctx.req.param('sortReverse')) === 1;
const page = ctx.req.param('page') ? Number.parseInt(ctx.req.param('page')) : 1;
const limit = ctx.req.query('limit') ?? 25;

View File

@@ -1,6 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '北京师范大学',
name: '北京林业大学',
url: 'graduate.bjfu.edu.cn',
};

View File

@@ -1,8 +1,23 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
import { Route } from '@/types';
import got from '@/utils/got';
import * as cheerio from 'cheerio';
module.exports = async (ctx) => {
const url = 'http://blogread.cn/news/newest.php';
export const route: Route = {
path: '/newest',
categories: ['programming'],
example: '/blogread/newest',
radar: [
{
source: ['blogread.cn/news/newest.php'],
},
],
name: '最新文章',
maintainers: ['fashioncj'],
handler,
};
async function handler() {
const url = 'https://blogread.cn/news/newest.php';
const response = await got({
method: 'get',
url,
@@ -12,19 +27,19 @@ module.exports = async (ctx) => {
.map((index, elem) => {
elem = $(elem);
const $link = elem.find('dt a');
return {
title: $link.text(),
description: elem.find('dd').eq(0).text(),
link: $link.attr('href'),
author: elem.find('.small a').eq(0).text(),
pubDate: elem.find('dd').eq(1).text().split('\n')[2],
};
})
.get();
ctx.state.data = {
return {
title: '技术头条',
link: url,
item: resultItem,
};
};
}

View File

@@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '技术头条',
url: 'blogread.cn',
};

View File

@@ -99,7 +99,7 @@ const parseArticle = (item) =>
res = await got(apiUrl, { headers });
} catch (error) {
// fallback
if (error.name && (error.name === 'HTTPError' || error.name === 'RequestError')) {
if (error.name && (error.name === 'HTTPError' || error.name === 'RequestError' || error.name === 'FetchError')) {
try {
res = await got(item.link, { headers });
} catch {
@@ -214,7 +214,7 @@ const parseReactRendererPage = async (res, api, item) => {
return await parseStoryJson(res.data, item);
} catch (error) {
// fallback
if (error.name && (error.name === 'HTTPError' || error.name === 'RequestError')) {
if (error.name && (error.name === 'HTTPError' || error.name === 'RequestError' || error.name === 'FetchError')) {
return {
title: item.title,
link: item.link,

View File

@@ -1,6 +1,6 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import cherrio from 'cheerio';
import * as cheerio from 'cheerio';
import { parseDate } from '@/utils/parse-date';
import puppeteer from '@/utils/puppeteer';
@@ -43,7 +43,7 @@ async function handler() {
const res = await page.evaluate(() => document.documentElement.innerHTML);
await page.close();
const $ = cherrio.load(res);
const $ = cheerio.load(res);
const items = $('div h3 a')
.toArray()
@@ -67,7 +67,7 @@ async function handler() {
waitUntil: 'domcontentloaded',
});
const res = await page.evaluate(() => document.documentElement.innerHTML);
const $ = cherrio.load(res);
const $ = cheerio.load(res);
await page.close();
item.description = $('div.article__body').html();

56
lib/routes/bnu/jwb.ts Normal file
View File

@@ -0,0 +1,56 @@
import got from '@/utils/got';
import { load } from 'cheerio';
import { parseDate } from '@/utils/parse-date';
import cache from '@/utils/cache';
export const route: Route = {
path: '/jwb',
categories: ['university'],
example: '/bnu/jwb',
parameters: {},
radar: [
{
source: ['jwb.bnu.edu.cn'],
},
],
name: '教务部(研究生院)',
maintainers: ['ladeng07'],
handler,
url: 'jwb.bnu.edu.cn/tzgg/index.htm',
};
async function handler() {
const link = 'https://jwb.bnu.edu.cn/tzgg/index.htm';
const response = await got(link);
const $ = load(response.data);
const list = $('.article-list .boxlist ul li')
.toArray()
.map((e) => {
e = $(e);
const a = e.find('a');
return {
title: e.find('a span').text(),
link: a.attr('href').startsWith('http') ? a.attr('href') : 'https://jwb.bnu.edu.cn' + a.attr('href').substring(2),
pubDate: parseDate(e.find('span.fr.text-muted').text(), 'YYYY-MM-DD'),
};
});
const out = await Promise.all(
list.map((item) =>
cache.tryGet(item.link, async () => {
const response = await got(item.link);
const $ = load(response.data);
item.author = '北京师范大学教务部';
item.description = $('.contenttxt').html();
return item;
})
)
);
return {
title: '北京师范大学教务部',
link,
description: '北京师范大学教务部最新通知',
item: out,
};
}

View File

@@ -1,4 +1,7 @@
import { Route } from '@/types';
import { getCurrentPath } from '@/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);
import got from '@/utils/got';
import queryString from 'query-string';
import { load } from 'cheerio';
@@ -21,9 +24,11 @@ export const route: Route = {
supportPodcast: false,
supportScihub: false,
},
radar: {
source: ['mmda.booru.org/index.php'],
},
radar: [
{
source: ['mmda.booru.org/index.php'],
},
],
name: 'MMDArchive 标签查询',
maintainers: ['N78Wy'],
handler,

View File

@@ -1,6 +1,6 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import cherrio from 'cheerio';
import * as cheerio from 'cheerio';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
@@ -34,7 +34,7 @@ async function handler() {
const url = `${homepage}/f/article/articleList?pageNo=1&pageSize=15&createTimeSort=DESC`;
const response = await got(url);
const $ = cherrio.load(response.data);
const $ = cheerio.load(response.data);
const articles = $('.aw-item').toArray();
const items = await Promise.all(
@@ -45,7 +45,7 @@ async function handler() {
return cache.tryGet(link, async () => {
const result = await got(link);
const $ = cherrio.load(result.data);
const $ = cheerio.load(result.data);
return {
title,
author: $('.user_name').text(),

View File

@@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'dbaplus社群',
url: 'dbaplus.cn',
};

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

@@ -0,0 +1,48 @@
import { Route } from '@/types';
import { parseDate } from '@/utils/parse-date';
import got from '@/utils/got';
import { load } from 'cheerio';
export const route: Route = {
path: '/',
categories: ['programming'],
example: '/dbaplus',
radar: [
{
source: ['dbaplus.cn/'],
},
],
name: '最新文章',
maintainers: ['cnkmmk'],
handler,
url: 'dbaplus.cn/',
};
async function handler() {
const url = 'https://dbaplus.cn';
const response = await got(`${url}/news-9-1.html`);
const $ = load(response.data);
const list = $('div.col-xs-12.col-md-8.pd30 > div.panel.panel-default.categeay > div.panel-body > ul.media-list.clearfix > li.media')
.map((i, e) => {
const element = $(e);
const title = element.find('h3 > a').text();
const link = element.find('h3 > a').attr('href');
const description = element.find('div.mt10.geay').text();
const dateraw = element.find('span.time').text();
return {
title,
description,
link,
pubDate: parseDate(dateraw, 'YYYY年MM月DD日'),
};
})
.get();
return {
title: 'dbaplus社群',
link: url,
item: list,
};
}

View File

@@ -39,7 +39,7 @@ export const route: Route = {
async function handler(ctx) {
if (!config.discord || !config.discord.authorization) {
throw new Error('Discord RSS is disabled due to the lack of <a href="https://docs.rsshub.app/en/install/#configuration-route-specific-configurations">relevant config</a>');
throw new Error('Discord RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const { authorization } = config.discord;
const channelId = ctx.req.param('channelId');

View File

@@ -2,7 +2,7 @@ import { config } from '@/config';
function getConfig(ctx) {
if (!config.discourse.config[ctx.req.param('configId')]) {
throw new Error('Discourse RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install">relevant config</a>');
throw new Error('Discourse RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/">relevant config</a>');
}
return config.discourse.config[ctx.req.param('configId')];
}

View File

@@ -18,8 +18,8 @@ export const route: Route = {
maintainers: ['daijinru', 'hestudy'],
handler,
description: `| javascript | node | react |
| ---------- | ---- | ----- |
| js | node | react |`,
| ---------- | ---- | ----- |
| js | node | react |`,
radar: [
{
source: ['docschina.org/news/weekly/js/*', 'docschina.org/news/weekly/js', 'docschina.org/'],

View File

@@ -22,7 +22,7 @@ export const route: Route = {
async function handler(ctx) {
if (!EhAPI.has_cookie) {
throw new Error('Ehentai favorites RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('Ehentai favorites RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const favcat = ctx.req.param('favcat') ? Number.parseInt(ctx.req.param('favcat')) : 0;
const page = ctx.req.param('page');

View File

@@ -13,13 +13,13 @@ export const route: Route = {
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
antiCrawler: true,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: 'GNN 新聞',
maintainers: ['Arracc'],
maintainers: ['Arracc', 'ladeng07'],
handler,
description: `| 首頁 | PC | TV 掌機 | 手機遊戲 | 動漫畫 | 主題報導 | 活動展覽 | 電競 |
| ---- | -- | ------- | -------- | ------ | -------- | -------- | ---- |
@@ -68,11 +68,13 @@ async function handler(ctx) {
url,
});
const data = response.data;
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 50;
const $ = load(data);
const list = $('div.BH-lbox.GN-lbox2')
.children()
.not('p,a,img,span')
.slice(0, limit)
.map((index, item) => {
item = $(item);
let aLabelNode;
@@ -99,11 +101,11 @@ async function handler(ctx) {
item.description = await cache.tryGet(item.link, async () => {
const response = await got.get(item.link);
let component = '';
const urlReg = /window.location.replace\('.*'/g;
const urlReg = /window\.lazySizesConfig/g;
let pubInfo;
let dateStr;
if (response.body.search(urlReg) < 0) {
if (response.body.search(urlReg) >= 0) {
const $ = load(response.data);
if ($('span.GN-lbox3C').length > 0) {
// official publish 1
@@ -119,8 +121,7 @@ async function handler(ctx) {
component = $('div.GN-lbox3B').html();
} else {
// url redirect
const newUrl = response.body.match(urlReg)[0].split('(')[1].replaceAll("'", '');
const _response = await got.get(newUrl);
const _response = await got.get(item.link);
const _$ = load(_response.data);
if (_$('div.MSG-list8C').length > 0) {

View File

@@ -0,0 +1,85 @@
import { Data, DataItem, Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
import { Context } from 'hono';
export const route: Route = {
path: '/:category?/:order?',
categories: ['game'],
example: '/gamer520/switchyouxi',
parameters: {
category: '分类,见下表',
order: '排序,发布日期: date; 修改日期: modified',
},
features: {
antiCrawler: true,
},
name: '文章',
maintainers: ['xzzpig'],
handler,
url: 'www.gamer520.com/',
description: `分类
| 所有 | Switch 游戏下载 | 金手指 | 3A 巨作 | switch 主题 | PC 游戏 |
| ---- | --------------- | ---------- | ------- | ----------- | ------- |
| all | switchyouxi | jinshouzhi | 3ajuzuo | zhuti | pcgame |`,
};
interface Post {
id: number;
guid: { rendered: string };
title: { rendered: string };
date_gmt: string;
modified_gmt: string;
categories?: number[];
content: { rendered: string };
}
interface Category {
id: number;
name: string;
link: string;
slug: string;
}
async function getCategories(baseUrl: string): Promise<Category[]> {
return (await cache.tryGet('gamer520:categories', async () => {
const { data } = await got(`${baseUrl}/wp-json/wp/v2/categories`);
return data.map((category) => ({ slug: category.slug, id: category.id, name: category.name, link: category.link }));
})) as Category[];
}
async function handler(ctx: Context): Promise<Data> {
const baseUrl = 'https://www.gamer520.com';
const categories = await getCategories(baseUrl);
const category = ctx.req.param('category') ?? 'all';
const order = ctx.req.param('order');
const categoryId = categories.find((c) => c.slug === category)?.id;
const { data } = (await got(`${baseUrl}/wp-json/wp/v2/posts`, {
searchParams: {
categories: categoryId,
orderby: order,
per_page: ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit') as string) : undefined,
},
})) as unknown as { data: Post[] };
const items: DataItem[] = data.map((item) => ({
guid: `gamer520:${item.id}`,
title: item.title.rendered,
link: item.guid.rendered,
pubDate: parseDate(item.date_gmt),
updated: parseDate(item.modified_gmt),
category: item.categories?.map((c) => categories.find((ca) => ca.id === c)?.name ?? '').filter((c) => c !== '') ?? [],
description: item.content.rendered,
}));
return {
title: '全球游戏交流中心-' + (categories.find((c) => c.slug === category)?.name ?? '所有'),
link: categories.find((c) => c.slug === category)?.link ?? baseUrl,
item: items,
};
}

View File

@@ -0,0 +1,6 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '全球游戏交流中心',
url: 'www.gamer520.com',
};

View File

@@ -5,14 +5,21 @@ import queryString from 'query-string';
export const route: Route = {
path: '/file/:user/:repo/:branch/:filepath{.+}',
example: '/github/file/DIYgod/RSSHub/master/README.md',
parameters: {
user: 'GitHub user or org name',
repo: 'repository name',
branch: 'branch name',
filepath: 'path of target file',
},
radar: [
{
source: ['github.com/:user/:repo/blob/:branch/*filepath'],
target: '/file/:user/:repo/:branch/:filepath',
},
],
name: 'Unknown',
maintainers: [],
name: 'File Commits',
maintainers: ['zengxs'],
handler,
};

View File

@@ -27,7 +27,7 @@ export const route: Route = {
async function handler(ctx) {
if (!config.github || !config.github.access_token) {
throw new Error('GitHub follower RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('GitHub follower RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const user = ctx.req.param('user');

View File

@@ -36,7 +36,7 @@ export const route: Route = {
async function handler(ctx) {
if (!config.github || !config.github.access_token) {
throw new Error('GitHub trending RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('GitHub trending RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const headers = {
Accept: 'application/vnd.github.v3+json',

View File

@@ -8,12 +8,12 @@ export const route: Route = {
example: '/github/stars/DIYGod/RSSHub',
parameters: { user: 'GitHub username', repo: 'GitHub repo name' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
requireConfig: [
{
name: 'GITHUB_ACCESS_TOKEN',
description: 'GitHub Access Token',
},
],
},
radar: [
{
@@ -27,7 +27,7 @@ export const route: Route = {
async function handler(ctx) {
if (!config.github || !config.github.access_token) {
throw new Error('GitHub star RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('GitHub star RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const user = ctx.req.param('user');
const repo = ctx.req.param('repo');

View File

@@ -1,6 +1,8 @@
import { Route } from '@/types';
import got from '@/utils/got';
import { config } from '@/config';
import md5 from '@/utils/md5';
import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/starred_repos/:user',
@@ -8,12 +10,13 @@ export const route: Route = {
example: '/github/starred_repos/DIYgod',
parameters: { user: 'User name' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
requireConfig: [
{
name: 'GITHUB_ACCESS_TOKEN',
optional: true,
description: 'To get more requests',
},
],
},
radar: [
{
@@ -26,62 +29,34 @@ export const route: Route = {
};
async function handler(ctx) {
if (!config.github || !config.github.access_token) {
throw new Error('GitHub star RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
}
const user = ctx.req.param('user');
const host = `https://github.com/${user}?tab=stars`;
const url = 'https://api.github.com/graphql';
const response = await got({
method: 'post',
url,
const { data: response } = await got(`https://api.github.com/users/${user}/starred`, {
headers: {
Authorization: `bearer ${config.github.access_token}`,
},
json: {
query: `
{
user(login: "${user}") {
starredRepositories(first: 10, orderBy: {direction: DESC, field: STARRED_AT}) {
edges {
starredAt
node {
name
description
url
openGraphImageUrl
primaryLanguage {
name
}
stargazers {
totalCount
}
}
}
}
}
}
`,
Accept: 'application/vnd.github.star+json',
Authorization: config.github?.access_token ? `Bearer ${config.github.access_token}` : undefined,
},
});
const data = response.data.data.user.starredRepositories.edges;
const data = response.map(({ starred_at, repo }) => ({
title: `${user} starred ${repo.name}`,
author: user,
description: `${repo.description ?? 'No Description'}<br>
Primary Language: ${repo.language ?? 'Primary Language'}<br>
Stargazers: ${repo.stargazers_count}<br>
<img sytle="width:50px;" src="https://opengraph.githubassets.com/${md5(repo.updated_at)}/${repo.full_name}">`,
pubDate: parseDate(starred_at),
link: repo.html_url,
category: repo.topics,
}));
return {
allowEmpty: true,
title: `${user}s starred repositories`,
title: `${user}'s starred repositories`,
link: host,
description: `${user}s starred repositories`,
item: data.map((repo) => ({
title: `${user} starred ${repo.node.name}`,
author: user,
description: `${repo.node.description === null ? 'no description' : repo.node.description} <br> primary language: ${
repo.node.primaryLanguage === null ? 'no primary language' : repo.node.primaryLanguage.name
} <br> stargazers: ${repo.node.stargazers.totalCount} <br> <img sytle="width:50px;" src='${repo.node.openGraphImageUrl}'>`,
pubDate: new Date(repo.starredAt),
link: repo.node.url,
})),
description: `${user}'s starred repositories`,
item: data,
};
}

View File

@@ -44,7 +44,7 @@ export const route: Route = {
async function handler(ctx) {
if (!config.github || !config.github.access_token) {
throw new Error('GitHub trending RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
throw new Error('GitHub trending RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
}
const since = ctx.req.param('since');
const language = ctx.req.param('language') === 'any' ? '' : ctx.req.param('language');

View File

@@ -42,7 +42,7 @@ export const route: Route = {
| date | trending | popularity | alpha | style |
:::warning
This route requires API key, therefore it's only available when self-hosting, refer to the [Deploy Guide](https://docs.rsshub.app/install/#configuration-route-specific-configurations) for route-specific configurations.
This route requires API key, therefore it's only available when self-hosting, refer to the [Deploy Guide](https://docs.rsshub.app/deploy/config#route-specific-configurations) for route-specific configurations.
:::`,
};

View File

@@ -56,7 +56,7 @@ async function handler() {
item.description = content('#fontzoom').html();
return item;
} catch (error) {
if (error.name === 'HTTPError') {
if (error.name === 'HTTPError' || error.name === 'FetchError') {
item.description = error.message;
return item;
}

View File

@@ -7,8 +7,8 @@ import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/mot/:category{.+}?',
name: 'Unknown',
maintainers: [],
name: '中华人民共和国交通运输部',
maintainers: ['ladeng07'],
handler,
};
@@ -48,7 +48,7 @@ async function handler(ctx) {
item.title = content('meta[name="ArticleTitle"]').prop('content') || content('h1#ti').text();
item.description = content('div.TRS_UEDITOR').html();
item.author = [...new Set([content('meta[name="Author"]').prop('content'), content('meta[name="ContentSource"]').prop('content')])].filter(Boolean);
item.author = [...new Set([content('meta[name="Author"]').prop('content'), content('meta[name="ContentSource"]').prop('content')])].find(Boolean);
item.category = [
...new Set([content('meta[name="ColumnName"]').prop('content'), content('meta[name="ColumnType"]').prop('content'), ...(content('meta[name="Keywords"]').prop('content')?.split(/,|;/) ?? [])]),
].filter(Boolean);

View File

@@ -1,15 +1,6 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
async function loadFullPage(id) {
const link = `https://apis.guokr.com/minisite/article/${id}.json`;
const content = await cache.tryGet(link, async () => {
const res = await got(link);
return res.data.result.content;
});
return content;
}
import { parseList, parseItem } from './utils';
const channelMap = {
calendar: 'pac',
@@ -19,21 +10,13 @@ const channelMap = {
};
export const route: Route = {
path: '/:channel',
path: '/column/:channel',
categories: ['new-media'],
example: '/guokr/calendar',
example: '/guokr/column/calendar',
parameters: { channel: '专栏类别' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['guokr.com/'],
source: ['guokr.com/:channel'],
},
],
name: '果壳网专栏',
@@ -48,29 +31,28 @@ export const route: Route = {
async function handler(ctx) {
const channel = channelMap[ctx.req.param('channel')] ?? ctx.req.param('channel');
const response = await got(`https://www.guokr.com/apis/minisite/article.json?retrieve_type=by_wx&channel_key=${channel}&offset=0&limit=10`);
const items = response.data.result;
const { data: response } = await got(`https://www.guokr.com/apis/minisite/article.json`, {
searchParams: {
retrieve_type: 'by_wx',
channel_key: channel,
offset: 0,
limit: 10,
},
});
const result = parseList(response.result);
if (items.length === 0) {
if (result.length === 0) {
throw new Error('Unknown channel');
}
const channel_name = items[0].channels[0].name;
const channel_url = items[0].channels[0].url;
const channelName = result[0].channels[0].name;
const channelUrl = result[0].channels[0].url;
const result = await Promise.all(
items.map(async (item) => ({
title: item.title,
description: await loadFullPage(item.id), // Mercury 无法正确解析全文,故这里手动加载
pubDate: item.date_published,
link: item.url,
author: item.author.nickname,
}))
);
const items = await Promise.all(result.map((item) => parseItem(item)));
return {
title: `果壳网 ${channel_name}`,
link: channel_url,
item: result,
title: `果壳网 ${channelName}`,
link: channelUrl,
item: items,
};
}

View File

@@ -1,21 +1,11 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
import { parseList, parseItem } from './utils';
export const route: Route = {
path: '/scientific',
categories: ['new-media'],
example: '/guokr/scientific',
parameters: {},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['guokr.com/scientific', 'guokr.com/'],
@@ -28,29 +18,21 @@ export const route: Route = {
};
async function handler() {
const response = await got('https://www.guokr.com/apis/minisite/article.json?retrieve_type=by_subject&limit=20&offset=0');
const { data: response } = await got('https://www.guokr.com/beta/proxy/science_api/articles', {
searchParams: {
retrieve_type: 'by_category',
page: 1,
},
});
const result = response.data.result;
const result = parseList(response);
const items = await Promise.all(result.map((item) => parseItem(item)));
return {
title: '果壳网 科学人',
link: 'https://www.guokr.com/scientific',
description: '果壳网 科学人',
item: await Promise.all(
result.map((item) =>
cache.tryGet(item.url, async () => {
const res = await got(item.url);
const $ = load(res.data);
item.description = $('.eflYNZ #js_content').css('visibility', 'visible').html() ?? $('.bxHoEL').html();
return {
title: item.title,
description: item.description,
pubDate: item.date_published,
link: item.url,
author: item.author.nickname,
};
})
)
),
item: items,
};
}

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