mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-03-13 10:30:18 +08:00
Merge remote-tracking branch 'origin/master' into feature/cloudflare-workers
This commit is contained in:
@@ -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
2
.github/FUNDING.yml
vendored
@@ -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']
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report_en.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report_en.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/rss_request_en.yml
vendored
2
.github/ISSUE_TEMPLATE/rss_request_en.yml
vendored
@@ -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!
|
||||
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -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: '/'
|
||||
|
||||
16
.github/workflows/build-assets.yml
vendored
16
.github/workflows/build-assets.yml
vendored
@@ -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
|
||||
|
||||
10
.github/workflows/comment-on-issue.yml
vendored
10
.github/workflows/comment-on-issue.yml
vendored
@@ -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 })
|
||||
|
||||
23
.github/workflows/docker-test-cont.yml
vendored
23
.github/workflows/docker-test-cont.yml
vendored
@@ -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...
|
||||
|
||||
2
.github/workflows/docker-test.yml
vendored
2
.github/workflows/docker-test.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/issue-command.yml
vendored
9
.github/workflows/issue-command.yml
vendored
@@ -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
44
.github/workflows/test-full-routes.yml
vendored
Normal 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
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
lib/routes-deprecated
|
||||
lib/router.js
|
||||
babel.config.js
|
||||
scripts/docker/minify-docker.js
|
||||
@@ -1 +1 @@
|
||||
## Please refer to [Join Us](https://docs.rsshub.app/joinus/quick-start)
|
||||
## Please refer to [Join Us](https://docs.rsshub.app/joinus/)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> <a href="https://xlog.app/" target="_blank"><img height="50px" src="https://i.imgur.com/JuhHTKD.png"></a>
|
||||
</p>
|
||||
|
||||
[](https://docs.rsshub.app/support/)
|
||||
[](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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import '@/utils/request-wrapper';
|
||||
import '@/utils/request-rewriter';
|
||||
|
||||
import { Hono } from 'hono';
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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('/'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
78
lib/routes.test.ts
Normal 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
156
lib/routes/005/index.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
};
|
||||
8
lib/routes/005/namespace.ts
Normal file
8
lib/routes/005/namespace.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: '幻之羁绊动漫网',
|
||||
url: '005.tv',
|
||||
categories: ['anime'],
|
||||
description: '',
|
||||
};
|
||||
27
lib/routes/005/templates/description.art
Normal file
27
lib/routes/005/templates/description.art
Normal 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 }}
|
||||
@@ -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'),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 }}">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, '')}`;
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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
121
lib/routes/1x/index.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
};
|
||||
8
lib/routes/1x/namespace.ts
Normal file
8
lib/routes/1x/namespace.ts
Normal 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.',
|
||||
};
|
||||
17
lib/routes/1x/templates/description.art
Normal file
17
lib/routes/1x/templates/description.art
Normal 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 }}
|
||||
@@ -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
121
lib/routes/3kns/index.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
6
lib/routes/3kns/namespace.ts
Normal file
6
lib/routes/3kns/namespace.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: '3k-Switch游戏库',
|
||||
url: 'www.3kns.com',
|
||||
};
|
||||
9
lib/routes/3kns/templates/description.art
Normal file
9
lib/routes/3kns/templates/description.art
Normal 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>
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import got from '@/utils/got';
|
||||
import type { Route } from '@/types';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/dynamic/:uid?',
|
||||
|
||||
6
lib/routes/appstorrent/namespace.ts
Normal file
6
lib/routes/appstorrent/namespace.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: 'AppsTorrent',
|
||||
url: 'appstorrent.ru',
|
||||
};
|
||||
91
lib/routes/appstorrent/programs.ts
Normal file
91
lib/routes/appstorrent/programs.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
22
lib/routes/appstorrent/templates/description.art
Normal file
22
lib/routes/appstorrent/templates/description.art
Normal 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}}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: '北京师范大学',
|
||||
name: '北京林业大学',
|
||||
url: 'graduate.bjfu.edu.cn',
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
6
lib/routes/blogread/namespace.ts
Normal file
6
lib/routes/blogread/namespace.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: '技术头条',
|
||||
url: 'blogread.cn',
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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
56
lib/routes/bnu/jwb.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
6
lib/routes/dbaplus/namespace.ts
Normal file
6
lib/routes/dbaplus/namespace.ts
Normal 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
48
lib/routes/dbaplus/rss.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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')];
|
||||
}
|
||||
|
||||
@@ -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/'],
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
85
lib/routes/gamer520/index.ts
Normal file
85
lib/routes/gamer520/index.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
6
lib/routes/gamer520/namespace.ts
Normal file
6
lib/routes/gamer520/namespace.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: '全球游戏交流中心',
|
||||
url: 'www.gamer520.com',
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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.
|
||||
:::`,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user