diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 11a6d970d557..de89bc644fcc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ // https://github.com/devcontainers/images/blob/v0.4.19/src/typescript-node/.devcontainer/devcontainer.json { "name": "Node.js & TypeScript", - "image": "mcr.microsoft.com/devcontainers/typescript-node:24-bookworm", + "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", "features": { "ghcr.io/devcontainers/features/docker-in-docker": { "version": "latest" @@ -40,7 +40,7 @@ } }, - "onCreateCommand": "sudo apt-get update && export DEBIAN_FRONTEND=noninteractive && sudo apt-get -y install --no-install-recommends ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libexpat1 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 wget xdg-utils redis-server default-jre-headless && sudo apt-get autoremove -y && sudo apt-get clean -y && sudo rm -rf /var/lib/apt/lists/*", + "onCreateCommand": "sudo apt-get update && export DEBIAN_FRONTEND=noninteractive && sudo apt-get -y install --no-install-recommends ca-certificates fonts-liberation libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 libcairo2 libcups2t64 libdbus-1-3 libexpat1 libgbm1 libglib2.0-0t64 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 wget xdg-utils redis-server default-jre-headless && sudo apt-get autoremove -y && sudo apt-get clean -y && sudo rm -rf /var/lib/apt/lists/*", "updateContentCommand": "export JAVA_HOME=/usr/lib/jvm/default-java && pnpm config set store-dir ~/.local/share/pnpm/store && pnpm i && pnpm rb && pnpx rebrowser-puppeteer browsers install chrome", diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 073aae6491d2..f80831c35b29 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,12 +10,15 @@ updates: - dependencies ignore: # pin to version before it is sold to potential suspicious party - # https://github.com/goofychris/art-template/issues/660 the issue created from original author stating v4.13.3 + # https://github.com/goofychris/art-template/issues/660 the issue created from original author stating v4.13.3 (March, 2025) # contains suspicious code and related issues (#658, #659) were deleted # related: # https://github.com/fastify/point-of-view/issues/463 https://github.com/fastify/point-of-view/pull/461#issuecomment-2718888986 # https://github.com/cnpm/bug-versions/pull/266 https://github.com/cnpm/cnpmcore/issues/777 # https://github.com/yoimiya-kokomi/Miao-Yunzai/pull/515 https://github.com/zhangfisher/flex-tools/commit/09b565dfe6e2932bb829613ddbe09f6d0acbccd4 + # v4.13.5, v4.13.6 (May, 2026) + # https://web.archive.org/web/20260521024725/https://github.com/goofychris/art-template/issues/665 + # https://safedep.io/art-template-npm-supply-chain-compromise/ https://github.com/ossf/malicious-packages/pull/1265 - dependency-name: art-template versions: ['>=4.13.3'] groups: diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml index 44e45a852264..5b03c411b6fd 100644 --- a/.github/workflows/build-assets.yml +++ b/.github/workflows/build-assets.yml @@ -18,9 +18,9 @@ jobs: contents: write steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install pnpm - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Use Node.js Active LTS uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: @@ -51,7 +51,7 @@ jobs: if: ${{ env.DOCS_API_TOKEN != '' }} run: echo "defined=true" >> $GITHUB_OUTPUT - name: Checkout docs - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 if: steps.check-docs-env.outputs.defined == 'true' with: repository: 'RSSNext/rsshub-docs' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a870789efa99..a0093b2d53a7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -48,7 +48,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # Initializes the CodeQL tools for scanning. # TODO: use hash pinning when https://github.com/dependabot/dependabot-core/pull/13007 pass diff --git a/.github/workflows/comment-on-issue.yml b/.github/workflows/comment-on-issue.yml index 06d6825c0088..2b973468ca6d 100644 --- a/.github/workflows/comment-on-issue.yml +++ b/.github/workflows/comment-on-issue.yml @@ -26,8 +26,8 @@ jobs: outputs: closed: ${{ steps.check.outputs.closed }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: lts/* @@ -60,8 +60,8 @@ jobs: (needs.checkIssue.result == 'success' || needs.checkIssue.result == 'skipped') && needs.checkIssue.outputs.closed != 'true' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: lts/* diff --git a/.github/workflows/dependabot-fork.yml b/.github/workflows/dependabot-fork.yml index 40494f4e0b40..3de657ec30d2 100644 --- a/.github/workflows/dependabot-fork.yml +++ b/.github/workflows/dependabot-fork.yml @@ -10,7 +10,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Comment Dependabot PR uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1 diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 9c542fad47ad..29838d4b5900 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -61,7 +61,7 @@ jobs: echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Extract repository name id: repo-name @@ -277,7 +277,7 @@ jobs: if: needs.check-env.outputs.check-docker == 'true' timeout-minutes: 5 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Docker Hub Description uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0 diff --git a/.github/workflows/docker-test-cont.yml b/.github/workflows/docker-test-cont.yml index 71de05f45bbe..66404d2b89a7 100644 --- a/.github/workflows/docker-test-cont.yml +++ b/.github/workflows/docker-test-cont.yml @@ -11,9 +11,10 @@ jobs: runs-on: ubuntu-latest permissions: pull-requests: write + actions: read if: ${{ github.event.workflow_run.conclusion == 'success' }} # skip if unsuccessful steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # https://github.com/orgs/community/discussions/25220#discussioncomment-11316244 - name: Search the PR that triggered this workflow @@ -42,7 +43,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: @@ -70,12 +71,12 @@ jobs: - name: Fetch Docker image if: (env.TEST_CONTINUE) - uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - workflow: ${{ github.event.workflow_run.workflow_id }} - run_id: ${{ github.event.workflow_run.id }} - name: docker-image + run-id: ${{ github.event.workflow_run.id }} + name: rsshub.tar.zst path: ../artifacts-${{ github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Import Docker image and set up Docker container if: (env.TEST_CONTINUE) diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 80e317c45364..6e7b68bd21a7 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -27,7 +27,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Docker Buildx # needed by `cache-from` uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 @@ -75,6 +75,6 @@ jobs: - name: Upload Docker image uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: docker-image + archive: false path: rsshub.tar.zst retention-days: 1 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 776717f4fc1c..6f6d483e15cd 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -14,8 +14,8 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: lts/* diff --git a/.github/workflows/ghcr-retention.yml b/.github/workflows/ghcr-retention.yml index ec98d38df410..eea0f5fc9258 100644 --- a/.github/workflows/ghcr-retention.yml +++ b/.github/workflows/ghcr-retention.yml @@ -13,7 +13,7 @@ jobs: contents: read steps: - name: Delete old container versions (30+ days) - uses: dataaxiom/ghcr-cleanup-action@f092b48ba3b604b2a83690dc4b2bbb3392e1045f # v1.2.1 + uses: dataaxiom/ghcr-cleanup-action@d52806a0dc70b430571a37da1fde39733ffd640f # v1.2.2 with: dry-run: false token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/issue-command.yml b/.github/workflows/issue-command.yml index 87e7edd967d4..cd4f7932a673 100644 --- a/.github/workflows/issue-command.yml +++ b/.github/workflows/issue-command.yml @@ -15,7 +15,7 @@ jobs: pull-requests: write steps: - name: Checkout the latest code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Automatic Rebase @@ -49,7 +49,7 @@ jobs: group: vouch-manage cancel-in-progress: false steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - id: vouch uses: mitchellh/vouch/action/manage-by-issue@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2 @@ -102,16 +102,16 @@ jobs: - name: Checkout if: ${{ !github.event.issue.pull_request }} - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Checkout PR if: github.event.issue.pull_request - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ fromJson(steps.pr-data.outputs.data).head.ref }} - name: Install pnpm - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Use Node.js Active LTS uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -120,7 +120,7 @@ jobs: cache: 'pnpm' - name: Install dependencies (pnpm) - run: pnpm i && pnpm rb && pnpm exec playwright install chromium + run: pnpm i && pnpm rb && pnpm exec patchright install chromium - name: Fetch affected routes id: fetch-route diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c269b1497144..4f3f64135197 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,8 +20,8 @@ jobs: permissions: security-events: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: lts/* @@ -80,7 +80,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Check if PR author is denounced uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 5dcd201dc749..769e6593544c 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -22,8 +22,8 @@ jobs: env: HUSKY: 0 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: lts/* diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index 512b7b734d51..fae0adbafcf7 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -22,7 +22,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # https://github.com/orgs/community/discussions/25220#discussioncomment-11316244 - name: Search the PR that triggered this workflow @@ -104,6 +104,7 @@ jobs: "grep*": "allow", "head*": "allow", "ls*": "allow", + "rg*": "allow", "sed*": "allow", "tail*": "allow", "wc*": "allow" diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index b7bda03ddc4f..083e4e78991e 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -22,7 +22,7 @@ jobs: permissions: security-events: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - run: semgrep ci --sarif > semgrep.sarif env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} diff --git a/.github/workflows/similar-issues.yml b/.github/workflows/similar-issues.yml index 95372a91e63f..b25530e85cf1 100644 --- a/.github/workflows/similar-issues.yml +++ b/.github/workflows/similar-issues.yml @@ -18,7 +18,7 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 diff --git a/.github/workflows/test-full-routes.yml b/.github/workflows/test-full-routes.yml index bf306839795d..7d6d41c0332e 100644 --- a/.github/workflows/test-full-routes.yml +++ b/.github/workflows/test-full-routes.yml @@ -14,9 +14,9 @@ jobs: contents: write steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install pnpm - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Use Node.js Active LTS uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aedd27b554ed..a4c72db8e1dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,12 +27,11 @@ jobs: strategy: fail-fast: false matrix: - # playwright install hangs in node v26.1.0, https://github.com/microsoft/playwright/issues/40724 - node-version: [26.0.0, lts/*, lts/-1] + node-version: [latest, lts/*, lts/-1] name: Vitest on Node ${{ matrix.node-version }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ matrix.node-version }} @@ -40,7 +39,7 @@ jobs: - name: Install dependencies (pnpm) run: pnpm i - name: Run postinstall script for dependencies - run: pnpm rb && pnpm exec playwright install chromium + run: pnpm rb && pnpm exec patchright install chromium - name: Build routes run: pnpm build - name: Build worker routes @@ -53,7 +52,7 @@ jobs: REDIS_URL: redis://localhost:${{ job.services.redis.ports['6379'] }}/ - name: Upload coverage to Codecov if: ${{ matrix.node-version == 'lts/*' }} - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos as documented, but seems broken @@ -63,7 +62,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [26.0.0, lts/*, lts/-1] + node-version: [latest, lts/*, lts/-1] chromium: - name: bundled Chromium dependency: '' @@ -76,8 +75,8 @@ jobs: environment: '{ "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD": "1" }' name: Vitest Playwright on Node ${{ matrix.node-version }} with ${{ matrix.chromium.name }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ matrix.node-version }} @@ -91,12 +90,12 @@ jobs: run: pnpm build - name: Install bundled Chromium if: ${{ matrix.chromium.dependency == '' }} - run: pnpm exec playwright install chromium + run: pnpm exec patchright install chromium - name: Install Chromium if: ${{ matrix.chromium.dependency != '' }} # 'chromium-browser' from Ubuntu APT repo is a dummy package. Its version (85.0.4183.83) means # nothing since it calls Snap (disgusting!) to install Chromium, which should be up-to-date. - # That's not really a problem since the Chromium-bundled Docker image is based on Debian bookworm, + # That's not really a problem since the Chromium-bundled Docker image is based on Debian trixie, # which provides up-to-date native packages. run: | set -eux @@ -125,8 +124,8 @@ jobs: node-version: [26, 24, 22] name: Build radar and maintainer on Node ${{ matrix.node-version }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ matrix.node-version }} @@ -148,7 +147,7 @@ jobs: pull-requests: write contents: write steps: - - uses: fastify/github-action-merge-dependabot@30c3f8f14a4f7b315ba38dbc1b793d27128fef82 # v3.12.0 + - uses: fastify/github-action-merge-dependabot@d1b52cc4c7e618b19de8a43c7138213b277e820c # v3.14.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} target: patch diff --git a/.github/workflows/update-nix-hash.yml b/.github/workflows/update-nix-hash.yml index 51f1f97812c6..8a400f38a727 100644 --- a/.github/workflows/update-nix-hash.yml +++ b/.github/workflows/update-nix-hash.yml @@ -18,7 +18,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install Nix uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 with: diff --git a/.oxlintrc.json b/.oxlintrc.json index 85967256d352..d9f0abc3a043 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -14,10 +14,10 @@ "jsPlugins": [ { "name": "n", "specifier": "eslint-plugin-n" }, { "name": "unicorn-js", "specifier": "eslint-plugin-unicorn" }, + { "name": "regexp", "specifier": "eslint-plugin-regexp" }, "@stylistic/eslint-plugin", "eslint-plugin-simple-import-sort", - "oxlint-plugin-eslint", - "./eslint-plugins/no-then.js", + { "name": "eslint-js", "specifier": "oxlint-plugin-eslint" }, "./eslint-plugins/nsfw-flag.js" ], "rules": { @@ -32,19 +32,19 @@ "no-const-assign": "error", "no-constant-binary-expression": "error", "no-constant-condition": "error", - // "no-control-regex": "error", -> off + "no-control-regex": "error", "no-debugger": "error", "no-dupe-class-members": "error", "no-dupe-else-if": "error", "no-dupe-keys": "error", "no-duplicate-case": "error", - "no-empty-character-class": "error", + // "no-empty-character-class": "error", -> off, handled by eslint-plugin-regexp "no-empty-pattern": "error", "no-ex-assign": "error", "no-fallthrough": "error", "no-func-assign": "error", "no-import-assign": "error", - "no-invalid-regexp": "error", + // "no-invalid-regexp": "error", -> off, handled by eslint-plugin-regexp "no-irregular-whitespace": "error", "no-loss-of-precision": "error", "no-misleading-character-class": "error", @@ -65,7 +65,7 @@ "no-unsafe-optional-chaining": "error", "no-unused-private-class-members": "error", // "no-unused-vars": "error", -> off for @typescript-eslint/no-unused-vars - "no-useless-backreference": "error", + // "no-useless-backreference": "error", -> off, handled by eslint-plugin-regexp "use-isnan": "error", "valid-typeof": "error", // #endregion @@ -148,45 +148,76 @@ // #endregion // #region --- unicorn recommended --- + // "unicorn-js/better-dom-traversing": "error", false positive on cheerio "unicorn/catch-error-name": "error", + "unicorn-js/class-reference-in-static-methods": "error", + // "unicorn-js/comment-content": "error", re-evaluate when unicorn > 66.0.0 "unicorn/consistent-assert": "error", + "unicorn-js/consistent-class-member-order": "error", + "unicorn-js/consistent-compound-words": "error", "unicorn/consistent-date-clone": "error", "unicorn/consistent-empty-array-spread": "error", "unicorn/consistent-existence-index-check": "error", - // "unicorn/consistent-function-scoping": "error", + "unicorn-js/consistent-export-decorator-position": "error", + // "unicorn/consistent-function-scoping": "error", -> warn + "unicorn-js/consistent-json-file-read": "error", + "unicorn-js/consistent-optional-chaining": "error", "unicorn/consistent-template-literal-escape": "error", "unicorn/empty-brace-spaces": "error", "unicorn/error-message": "error", "unicorn/escape-case": "error", // "unicorn/expiring-todo-comments": "error", // not yet implemented // "unicorn/explicit-length-check": "error", + "unicorn-js/explicit-timer-delay": "error", // "unicorn/filename-case": "error", "unicorn/import-style": "error", - "unicorn-js/isolated-functions": "error", // use jsPlugins + "unicorn-js/isolated-functions": "error", + "unicorn-js/max-nested-calls": "error", "unicorn/new-for-builtins": "error", "unicorn/no-abusive-eslint-disable": "error", "unicorn/no-accessor-recursion": "error", "unicorn/no-anonymous-default-export": "error", // "unicorn/no-array-callback-reference": "error", + "unicorn/no-array-fill-with-reference-type": "error", "unicorn/no-array-for-each": "error", + "unicorn-js/no-array-from-fill": "error", "unicorn/no-array-method-this-argument": "error", // "unicorn/no-array-reduce": "error", "unicorn/no-array-reverse": "error", // "unicorn/no-array-sort": "error", // "unicorn/no-await-expression-member": "error", "unicorn/no-await-in-promise-methods": "error", + "unicorn-js/no-blob-to-file": "error", + // "unicorn-js/no-break-in-nested-loop": "error", unopinionated + // "unicorn-js/no-canvas-to-image": "error", // unused + "unicorn-js/no-computed-property-existence-check": "error", + "unicorn-js/no-confusing-array-splice": "error", + "unicorn-js/no-confusing-array-with": "error", "unicorn/no-console-spaces": "error", + // "unicorn-js/no-declarations-before-early-exit": "error", re-evaluate when unicorn > 66.0.0 "unicorn/no-document-cookie": "error", + "unicorn-js/no-duplicate-loops": "error", + "unicorn-js/no-duplicate-set-values": "error", // "unicorn/no-empty-file": "error", + "unicorn-js/no-error-property-assignment": "error", + "unicorn-js/no-exports-in-scripts": "error", // "unicorn/no-for-loop": "error", // won't be implemented + "unicorn-js/no-global-object-property-assignment": "error", // "unicorn/no-hex-escape": "error", "unicorn/no-immediate-mutation": "error", + "unicorn-js/no-incorrect-query-selector": "error", + // "unicorn-js/no-incorrect-template-string-interpolation": "error", too many false positives "unicorn/no-instanceof-builtins": "error", + "unicorn-js/no-invalid-argument-count": "error", "unicorn/no-invalid-fetch-options": "error", "unicorn/no-invalid-remove-event-listener": "error", + "unicorn-js/no-late-current-target-access": "error", "unicorn/no-lonely-if": "error", "unicorn/no-magic-array-flat-depth": "error", + "unicorn-js/no-mismatched-map-key": "error", // "unicorn/no-named-default": "error", -> use import/no-named-default + "unicorn-js/no-negated-array-predicate": "error", + "unicorn-js/no-negated-comparison": "error", "no-negated-condition": "off", "unicorn/no-negated-condition": "error", "unicorn/no-negation-in-equality-check": "error", @@ -196,38 +227,64 @@ "unicorn/no-new-buffer": "error", // "unicorn/no-null": "error", // "unicorn/no-object-as-default-parameter": "error", + "unicorn-js/no-object-methods-with-collections": "error", + "unicorn-js/no-optional-chaining-on-undeclared-variable": "error", // "unicorn/no-process-exit": "error", + "unicorn-js/no-redundant-comparison": "error", + "unicorn-js/no-return-array-push": "error", "unicorn/no-single-promise-in-promise-methods": "error", "unicorn/no-static-only-class": "error", + "unicorn-js/no-subtraction-comparison": "error", "unicorn/no-thenable": "error", "unicorn/no-this-assignment": "error", + "unicorn-js/no-this-outside-of-class": "error", + // "unicorn-js/no-top-level-side-effects": "error", // allows dayjs.extend() "unicorn/no-typeof-undefined": "error", + "unicorn-js/no-undeclared-class-members": "error", "unicorn/no-unnecessary-array-flat-depth": "error", "unicorn/no-unnecessary-array-splice-count": "error", "unicorn/no-unnecessary-await": "error", - "unicorn-js/no-unnecessary-polyfills": "error", // use jsPlugins + "unicorn-js/no-unnecessary-global-this": "error", + "unicorn-js/no-unnecessary-nested-ternary": "error", + "unicorn-js/no-unnecessary-polyfills": "error", "unicorn/no-unnecessary-slice-end": "error", + "unicorn-js/no-unnecessary-splice": "error", "unicorn/no-unreadable-array-destructuring": "error", "unicorn/no-unreadable-iife": "error", + // "unicorn-js/no-unreadable-new-expression": "error", // allows new URL(x).href + "unicorn-js/no-unreadable-object-destructuring": "error", + "unicorn-js/no-unsafe-buffer-conversion": "error", + "unicorn-js/no-unsafe-property-key": "error", + "unicorn-js/no-unsafe-string-replacement": "error", + "unicorn-js/no-unused-array-method-return": "error", + "unicorn-js/no-useless-boolean-cast": "error", "unicorn/no-useless-collection-argument": "error", + "unicorn-js/no-useless-concat": "error", + "unicorn-js/no-useless-else": "error", "unicorn/no-useless-error-capture-stack-trace": "error", "unicorn/no-useless-fallback-in-spread": "error", // "unicorn/no-useless-iterator-to-array": "error", "unicorn/no-useless-length-check": "error", "unicorn/no-useless-promise-resolve-reject": "error", + // "unicorn-js/no-useless-recursion": "error", unopinionated "unicorn/no-useless-spread": "error", // "unicorn/no-useless-switch-case": "error", + "unicorn-js/no-useless-template-literals": "error", // "unicorn/no-useless-undefined": "error", "unicorn/no-zero-fractions": "error", // "unicorn/number-literal-case": "error", // "unicorn/numeric-separators-style": "error", "unicorn/prefer-add-event-listener": "error", + "unicorn-js/prefer-add-event-listener-options": "error", "unicorn/prefer-array-find": "error", "unicorn/prefer-array-flat": "error", "unicorn/prefer-array-flat-map": "error", + "unicorn-js/prefer-array-from-map": "error", "unicorn/prefer-array-index-of": "error", + "unicorn-js/prefer-array-last-methods": "error", "unicorn/prefer-array-some": "error", "unicorn/prefer-at": "error", + "unicorn-js/prefer-await": "error", "unicorn/prefer-bigint-literals": "error", "unicorn/prefer-blob-reading-methods": "error", "unicorn/prefer-class-fields": "error", @@ -235,66 +292,166 @@ // "unicorn/prefer-code-point": "error", "unicorn/prefer-date-now": "error", "unicorn/prefer-default-parameters": "error", + "unicorn-js/prefer-direct-iteration": "error", "unicorn/prefer-dom-node-append": "error", "unicorn/prefer-dom-node-dataset": "error", "unicorn/prefer-dom-node-remove": "error", "unicorn/prefer-dom-node-text-content": "error", + "unicorn-js/prefer-early-return": "error", "unicorn/prefer-event-target": "error", - "unicorn-js/prefer-export-from": "error", // use jsPlugins + "unicorn/prefer-export-from": "error", + "unicorn-js/prefer-get-or-insert-computed": "error", + "unicorn-js/prefer-global-number-constants": "error", // "unicorn/prefer-global-this": "error", + // "unicorn-js/prefer-https": "error", + "unicorn-js/prefer-identifier-import-export-specifiers": "error", "unicorn/prefer-includes": "error", + "unicorn-js/prefer-includes-over-repeated-comparisons": "error", + "unicorn-js/prefer-iterable-in-constructor": "error", + "unicorn-js/prefer-iterator-to-array": "error", + // "unicorn-js/prefer-iterator-to-array-at-end": "error", "unicorn/prefer-keyboard-event-key": "error", + "unicorn-js/prefer-location-assign": "error", "unicorn/prefer-logical-operator-over-ternary": "error", + "unicorn-js/prefer-math-abs": "error", "unicorn/prefer-math-min-max": "error", "unicorn/prefer-math-trunc": "error", + // "unicorn-js/prefer-minimal-ternary": "error", re-evaluate when unicorn > 66.0.0 "unicorn/prefer-modern-dom-apis": "error", "unicorn/prefer-modern-math-apis": "error", // "unicorn/prefer-module": "error", "unicorn/prefer-native-coercion-functions": "error", "unicorn/prefer-negative-index": "error", "unicorn/prefer-node-protocol": "error", - // "unicorn/prefer-number-properties": "error", + "unicorn-js/prefer-number-coercion": "error", + "unicorn-js/prefer-number-is-safe-integer": "error", + // "unicorn/prefer-number-properties": "error", // checkNaN: false + "unicorn-js/prefer-object-define-properties": "error", + "unicorn-js/prefer-object-destructuring-defaults": "error", "unicorn/prefer-object-from-entries": "error", + "unicorn-js/prefer-object-iterable-methods": "error", "unicorn/prefer-optional-catch-binding": "error", + // "unicorn-js/prefer-path2d": "error", // unused + "unicorn-js/prefer-private-class-fields": "error", "unicorn/prefer-prototype-methods": "error", "unicorn/prefer-query-selector": "error", + "unicorn-js/prefer-queue-microtask": "error", "unicorn/prefer-reflect-apply": "error", "unicorn/prefer-regexp-test": "error", "unicorn/prefer-response-static-json": "error", + "unicorn-js/prefer-scoped-selector": "error", "unicorn/prefer-set-has": "error", "unicorn/prefer-set-size": "error", - "unicorn-js/prefer-simple-condition-first": "error", // use jsPlugins - "unicorn-js/prefer-single-call": "error", // use jsPlugins + "unicorn-js/prefer-short-arrow-method": "error", + "unicorn-js/prefer-simple-condition-first": "error", + "unicorn-js/prefer-simple-sort-comparator": "error", + "unicorn-js/prefer-single-array-predicate": "error", + "unicorn/prefer-single-call": "error", + "unicorn-js/prefer-single-object-destructuring": "error", + "unicorn-js/prefer-smaller-scope": "error", + "unicorn-js/prefer-split-limit": "error", // "unicorn/prefer-spread": "error", + "unicorn-js/prefer-string-match-all": "error", + "unicorn-js/prefer-string-pad-start-end": "error", "unicorn/prefer-string-raw": "error", + "unicorn-js/prefer-string-repeat": "error", "unicorn/prefer-string-replace-all": "error", // "unicorn/prefer-string-slice": "error", - "unicorn/prefer-string-starts-ends-with": "error", + // "unicorn/prefer-string-starts-ends-with": "error", use typescript/prefer-string-starts-ends-with "unicorn/prefer-string-trim-start-end": "error", "unicorn/prefer-structured-clone": "error", - // "unicorn/prefer-switch": "error", // use jsPlugins + // "unicorn/prefer-switch": "error", "unicorn/prefer-ternary": "error", // "unicorn/prefer-top-level-await": "error", "unicorn/prefer-type-error": "error", + "unicorn-js/prefer-type-literal-last": "error", + // "unicorn-js/prefer-uint8array-base64": "error", re-evaluate when node > 25 + "unicorn-js/prefer-url-href": "error", // "unicorn/prevent-abbreviations": "error", "unicorn/relative-url-style": "error", "unicorn/require-array-join-separator": "error", + "unicorn-js/require-array-sort-compare": "error", + "unicorn-js/require-css-escape": "error", "unicorn/require-module-attributes": "error", "unicorn/require-module-specifiers": "error", "unicorn/require-number-to-fixed-digits-argument": "error", + "unicorn-js/require-passive-events": "error", + "unicorn-js/require-proxy-trap-boolean-return": "error", // "unicorn/switch-case-braces": "error", - "unicorn-js/switch-case-break-position": "error", // use jsPlugins - "unicorn-js/template-indent": "error", // use jsPlugins + "unicorn-js/switch-case-break-position": "error", + "unicorn-js/template-indent": "error", // "unicorn/text-encoding-identifier-case": "error", "unicorn/throw-new-error": "error", // #endregion + // #region --- regexp recommended --- + "regexp/confusing-quantifier": "warn", + "regexp/control-character-escape": "error", + "regexp/match-any": "error", + "regexp/negation": "error", + "regexp/no-contradiction-with-assertion": "error", + "regexp/no-dupe-characters-character-class": "error", + "regexp/no-dupe-disjunctions": "error", + "regexp/no-empty-alternative": "warn", + "regexp/no-empty-capturing-group": "error", + "regexp/no-empty-character-class": "error", + "regexp/no-empty-group": "error", + "regexp/no-empty-lookarounds-assertion": "error", + "regexp/no-empty-string-literal": "error", + "regexp/no-escape-backspace": "error", + "regexp/no-extra-lookaround-assertions": "error", + "regexp/no-invalid-regexp": "error", + "regexp/no-invisible-character": "error", + "regexp/no-lazy-ends": "warn", + "regexp/no-legacy-features": "error", + "regexp/no-misleading-capturing-group": "error", + "regexp/no-misleading-unicode-character": "error", + "regexp/no-missing-g-flag": "error", + "regexp/no-non-standard-flag": "error", + "regexp/no-obscure-range": "error", + "regexp/no-optional-assertion": "error", + "regexp/no-potentially-useless-backreference": "warn", + "regexp/no-super-linear-backtracking": "error", + "regexp/no-trivially-nested-assertion": "error", + "regexp/no-trivially-nested-quantifier": "error", + "regexp/no-unused-capturing-group": "error", + "regexp/no-useless-assertions": "error", + "regexp/no-useless-backreference": "error", + "regexp/no-useless-character-class": "error", + "regexp/no-useless-dollar-replacements": "error", + "regexp/no-useless-escape": "error", + "regexp/no-useless-flag": "warn", + "regexp/no-useless-lazy": "error", + "regexp/no-useless-non-capturing-group": "error", + "regexp/no-useless-quantifier": "error", + "regexp/no-useless-range": "error", + "regexp/no-useless-set-operand": "error", + "regexp/no-useless-string-literal": "error", + "regexp/no-useless-two-nums-quantifier": "error", + "regexp/no-zero-quantifier": "error", + "regexp/optimal-lookaround-quantifier": "warn", + "regexp/optimal-quantifier-concatenation": "error", + "regexp/prefer-character-class": "error", + "regexp/prefer-d": "error", + "regexp/prefer-plus-quantifier": "error", + "regexp/prefer-predefined-assertion": "error", + "regexp/prefer-question-quantifier": "error", + "regexp/prefer-range": "error", + "regexp/prefer-set-operation": "error", + "regexp/prefer-star-quantifier": "error", + "regexp/prefer-unicode-codepoint-escapes": "error", + "regexp/prefer-w": "error", + "regexp/simplify-set-operations": "error", + "regexp/sort-flags": "error", + "regexp/strict": "error", + "regexp/use-ignore-case": "error", + // #endregion + // --- custom rules --- // #region --- possible problems --- "array-callback-return": ["error", { "allowImplicit": true }], "no-await-in-loop": "error", - "no-control-regex": "off", "no-prototype-builtins": "off", "no-undef": "off", // typescript/eslint-recommended, ts(2552) // #endregion @@ -324,14 +481,14 @@ } ], - "eslint-js/no-implicit-globals": "error", // use jsPlugins + "eslint-js/no-implicit-globals": "error", "no-labels": "error", "no-lonely-if": "error", "no-multi-str": "error", "no-new-func": "error", "eslint-js/no-restricted-syntax": [ - "error", // use jsPlugins + "error", { "selector": "CallExpression[callee.property.name='get'][arguments.length=0]", "message": "Please use .toArray() instead." @@ -383,7 +540,7 @@ "no-useless-concat": "warn", "no-useless-rename": "error", "no-var": "error", - "eslint-js/object-shorthand": "error", // use jsPlugins + "eslint-js/object-shorthand": "error", "prefer-arrow-callback": "error", "prefer-const": "error", "prefer-object-has-own": "error", @@ -413,6 +570,8 @@ "@typescript-eslint/no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }], "@typescript-eslint/no-unused-vars": ["error", { "args": "after-used", "argsIgnorePattern": "^_" }], + "@typescript-eslint/prefer-string-starts-ends-with": "error", // type-aware + // type-aware // "@typescript-eslint/await-thenable": "off", // "@typescript-eslint/no-base-to-string": "off", @@ -559,9 +718,6 @@ ], // #endregion - // github - "github/no-then": "warn", - // rsshub "@rsshub/nsfw-flag/add-nsfw-flag": "error" }, diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 6c0244f8d43f..6e9b14134c6b 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,84 +1,83 @@ -# Contributor Covenant Code of Conduct +# Contributor Covenant 3.0 Code of Conduct ## Our Pledge -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. +We pledge to make our community welcoming, safe, and equitable for all. -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. +We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. -## Our Standards +## Encouraged Behaviors -Examples of behavior that contributes to a positive environment for our community include: +While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -- Focusing on what is best not just for us as individuals, but for the overall community +With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: -Examples of unacceptable behavior include: +1. Respecting the **purpose of our community**, our activities, and our ways of gathering. +2. Engaging **kindly and honestly** with others. +3. Respecting **different viewpoints** and experiences. +4. **Taking responsibility** for our actions and contributions. +5. Gracefully giving and accepting **constructive feedback**. +6. Committing to **repairing harm** when it occurs. +7. Behaving in other ways that promote and sustain the **well-being of our community**. -- The use of sexualized language or imagery, and sexual attention or - advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email - address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting +## Restricted Behaviors -## Enforcement Responsibilities +We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. -Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. +1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. +2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. +3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits. +4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. +5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. +6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. +7. Behaving in other ways that **threaten the well-being** of our community. -Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. +### Other Restrictions -## Scope - -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the reporter of any incident. - -## Enforcement Guidelines +1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. +2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. +3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. +4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. -Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: +## Reporting an Issue -### 1. Correction +Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. -**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. +When an incident does occur, it is important to report it promptly. To report a possible violation, send an email to . -**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. +Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. -### 2. Warning +## Addressing and Repairing Harm -**Community Impact**: A violation through a single incident or series of actions. +If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. -**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. +1. Warning + 1. Event: A violation involving a single incident or series of incidents. + 2. Consequence: A private, written warning from the Community Moderators. + 3. Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. +2. Temporarily Limited Activities + 1. Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. + 2. Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. + 3. Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. +3. Temporary Suspension + 1. Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. + 2. Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. + 3. Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. +4. Permanent Ban + 1. Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. + 2. Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. + 3. Repair: There is no possible repair in cases of this severity. -### 3. Temporary Ban +This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. -**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. +## Scope -**Consequence**: A permanent ban from any sort of public interaction within the project community. +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, -available at . - -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). +This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). -[homepage]: https://www.contributor-covenant.org +Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) -For answers to common questions about this code of conduct, see the FAQ at -. Translations are available at . +For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion). diff --git a/Dockerfile b/Dockerfile index 6683193b7ed6..e79e017996a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24-bookworm AS dep-builder +FROM node:24-trixie AS dep-builder # Here we use the non-slim image to provide build-time deps (compilers and python), thus no need to install later. # This effectively speeds up qemu-based cross-build. @@ -30,23 +30,23 @@ RUN \ # --------------------------------------------------------------------------------------------------------------------- -FROM debian:bookworm-slim AS dep-version-parser +FROM debian:trixie-slim AS dep-version-parser # This stage is necessary to limit the cache miss scope. # With this stage, any modification to package.json won't break the build cache of the next two stages as long as the # version unchanged. -# node:24-bookworm-slim is based on debian:bookworm-slim so this stage would not cause any additional download. +# node:24-trixie-slim is based on debian:trixie-slim so this stage would not cause any additional download. WORKDIR /ver COPY ./package.json /app/ RUN \ set -ex && \ - grep -Po '(?<="playwright": ")[^\s"]*(?=")' /app/package.json | tee /ver/.playwright_version && \ + grep -Po '(?<="patchright": ")[^\s"]*(?=")' /app/package.json | tee /ver/.patchright_version && \ grep -Po '(?<="@vercel/nft": ")[^\s"]*(?=")' /app/package.json | tee /ver/.nft_version && \ grep -Po '(?<="fs-extra": ")[^\s"]*(?=")' /app/package.json | tee /ver/.fs_extra_version # --------------------------------------------------------------------------------------------------------------------- -FROM node:24-bookworm-slim AS docker-minifier +FROM node:24-trixie-slim AS docker-minifier # The stage is used to further reduce the image size by removing unused files. WORKDIR /minifier @@ -83,17 +83,17 @@ RUN \ # --------------------------------------------------------------------------------------------------------------------- -FROM node:24-bookworm-slim AS chromium-downloader +FROM node:24-trixie-slim AS chromium-downloader # This stage is necessary to improve build concurrency and minimize the image size. # Yeah, downloading Chromium never needs those dependencies below. WORKDIR /app -COPY --from=dep-version-parser /ver/.playwright_version /app/.playwright_version +COPY --from=dep-version-parser /ver/.patchright_version /app/.patchright_version ARG TARGETPLATFORM ARG USE_CHINA_NPM_REGISTRY=0 ARG PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 -# The official recommended way to use Playwright on x86(_64) is to use the bundled browser. +# The official recommended way to use Patchright on x86(_64) is to use the bundled browser. RUN \ set -ex ; \ if [ "$PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD" = 0 ] && [ "$TARGETPLATFORM" = 'linux/amd64' ]; then \ @@ -106,16 +106,16 @@ RUN \ unset PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD && \ export PLAYWRIGHT_BROWSERS_PATH=/app/node_modules/.cache/ms-playwright && \ corepack enable pnpm && \ - pnpm --allow-build=playwright add playwright@$(cat /app/.playwright_version) --save-prod && \ + pnpm --allow-build=patchright --allow-build=patchright-core add patchright@$(cat /app/.patchright_version) --save-prod && \ pnpm rb && \ - pnpm exec playwright install chromium ; \ + pnpm exec patchright install chromium ; \ else \ mkdir -p /app/node_modules/.cache/ms-playwright ; \ fi; # --------------------------------------------------------------------------------------------------------------------- -FROM node:24-bookworm-slim AS app +FROM node:24-trixie-slim AS app LABEL org.opencontainers.image.authors="https://github.com/DIYgod/RSSHub" @@ -127,8 +127,7 @@ WORKDIR /app # install deps first to avoid cache miss or disturbing buildkit to build concurrently ARG TARGETPLATFORM ARG PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 -# https://playwright.dev/docs/docker#introduction -# https://www.debian.org/releases/bookworm/amd64/release-notes/ch-information.en.html#noteworthy-obsolete-packages +# https://playwright.dev/docs/library#browser-downloads # On arm/arm64, install Chromium from the distribution repositories. RUN \ set -ex && \ @@ -140,8 +139,8 @@ RUN \ if [ "$TARGETPLATFORM" = 'linux/amd64' ]; then \ apt-get install -yq --no-install-recommends \ ca-certificates fonts-liberation wget xdg-utils \ - libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 \ - libexpat1 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 \ + libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 libcairo2 libcups2t64 libdbus-1-3 libdrm2 \ + libexpat1 libgbm1 libglib2.0-0t64 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 \ libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 \ ; \ else \ diff --git a/eslint-plugins/no-then.js b/eslint-plugins/no-then.js deleted file mode 100644 index eef945cc34c9..000000000000 --- a/eslint-plugins/no-then.js +++ /dev/null @@ -1,45 +0,0 @@ -import { eslintCompatPlugin } from '@oxlint/plugins'; - -const rule = { - meta: { - type: 'suggestion', - docs: { - description: 'enforce using `async/await` syntax over Promises', - url: 'https://github.com/github/eslint-plugin-github/blob/main/docs/rules/no-then.md', - recommended: true, - }, - schema: [], - messages: { - preferAsyncAwait: 'Prefer async/await to Promise.{{method}}()', - }, - }, - - createOnce(context) { - return { - MemberExpression(node) { - if (node.property && node.property.name === 'then') { - context.report({ - node: node.property, - messageId: 'preferAsyncAwait', - data: { method: 'then' }, - }); - } else if (node.property && node.property.name === 'catch') { - context.report({ - node: node.property, - messageId: 'preferAsyncAwait', - data: { method: 'catch' }, - }); - } - }, - }; - }, -}; - -export default eslintCompatPlugin({ - meta: { - name: 'github', - }, - rules: { - 'no-then': rule, - }, -}); diff --git a/eslint-plugins/nsfw-flag.js b/eslint-plugins/nsfw-flag.js index 32c9397cd690..aed8717ad97f 100644 --- a/eslint-plugins/nsfw-flag.js +++ b/eslint-plugins/nsfw-flag.js @@ -119,96 +119,92 @@ export default eslintCompatPlugin({ missingNsfwFlag: 'NSFW route is missing the nsfw flag in features', }, }, - createOnce(context) { - return { - before() { - // 如果不是 NSFW 路由,跳过检查 - if (!isNsfwRoute(context.filename)) { - return false; - } - }, - ExportNamedDeclaration(node) { - // 查找 export const route: Route = {...} - if ( - node.declaration && - node.declaration.type === 'VariableDeclaration' && - node.declaration.declarations && - node.declaration.declarations[0] && - node.declaration.declarations[0].id && - node.declaration.declarations[0].id.name === 'route' - ) { - const routeDeclaration = node.declaration.declarations[0]; - const routeObject = routeDeclaration.init; + createOnce: (context) => ({ + before() { + // 如果不是 NSFW 路由,跳过检查 + if (!isNsfwRoute(context.filename)) { + return false; + } + }, + ExportNamedDeclaration(node) { + // 查找 export const route: Route = {...} + if ( + !node.declaration || + node.declaration.type !== 'VariableDeclaration' || + !node.declaration.declarations || + !node.declaration.declarations[0] || + !node.declaration.declarations[0].id || + node.declaration.declarations[0].id.name !== 'route' + ) { + return; + } + const routeDeclaration = node.declaration.declarations[0]; + const routeObject = routeDeclaration.init; - if (routeObject && routeObject.type === 'ObjectExpression') { - let featuresProperty = null; - let nsfwProperty = null; + if (routeObject && routeObject.type === 'ObjectExpression') { + let featuresProperty = null; + let nsfwProperty = null; - // 查找 features 属性 - for (const prop of routeObject.properties) { - if (prop.type === 'Property' && prop.key && prop.key.name === 'features') { - featuresProperty = prop; + // 查找 features 属性 + for (const prop of routeObject.properties) { + if (prop.type === 'Property' && prop.key && prop.key.name === 'features') { + featuresProperty = prop; - // 在 features 中查找 nsfw 属性 - if (prop.value && prop.value.type === 'ObjectExpression') { - for (const featureProp of prop.value.properties) { - if (featureProp.type === 'Property' && featureProp.key && featureProp.key.name === 'nsfw') { - nsfwProperty = featureProp; - break; - } - } + // 在 features 中查找 nsfw 属性 + if (prop.value && prop.value.type === 'ObjectExpression') { + for (const featureProp of prop.value.properties) { + if (featureProp.type === 'Property' && featureProp.key && featureProp.key.name === 'nsfw') { + nsfwProperty = featureProp; + break; } - break; } } + break; + } + } - // 检查是否需要添加或修复 nsfw 标志 - if (!featuresProperty) { - // 没有 features 属性,需要添加整个 features 对象 - context.report({ - node: routeObject, - messageId: 'missingNsfwFlag', - fix(fixer) { - // 在对象的最后添加 features 属性 - const lastProperty = routeObject.properties.at(-1); + // 检查是否需要添加或修复 nsfw 标志 + if (!featuresProperty) { + // 没有 features 属性,需要添加整个 features 对象 + context.report({ + node: routeObject, + messageId: 'missingNsfwFlag', + fix(fixer) { + // 在对象的最后添加 features 属性 + const lastProperty = routeObject.properties.at(-1); - return lastProperty - ? fixer.insertTextAfter(lastProperty, ',\n features: {\n nsfw: true,\n }') - : // 空对象的情况 - fixer.insertTextAfter(routeObject.properties.length > 0 ? routeObject.properties.at(-1) : routeObject, '\n features: {\n nsfw: true,\n }\n'); - }, - }); - } else if (!nsfwProperty) { - // 有 features 属性但没有 nsfw 属性 - context.report({ - node: featuresProperty.value, - messageId: 'missingNsfwFlag', - fix(fixer) { - const featuresObject = featuresProperty.value; - if (featuresObject.properties.length > 0) { - const lastFeatureProp = featuresObject.properties.at(-1); - return fixer.insertTextAfter(lastFeatureProp, ',\n nsfw: true'); - } else { - // features 是空对象 - return fixer.replaceTextRange([featuresObject.range[0] + 1, featuresObject.range[1] - 1], '\n nsfw: true,\n '); - } - }, - }); - } else if (nsfwProperty.value && (nsfwProperty.value.type !== 'Literal' || nsfwProperty.value.value !== true)) { - // nsfw 属性存在但不是 true - context.report({ - node: nsfwProperty.value, - messageId: 'missingNsfwFlag', - fix(fixer) { - return fixer.replaceText(nsfwProperty.value, 'true'); - }, - }); - } - } + return lastProperty + ? fixer.insertTextAfter(lastProperty, ',\n features: {\n nsfw: true,\n }') + : // 空对象的情况 + fixer.insertTextAfter(routeObject.properties.length > 0 ? routeObject.properties.at(-1) : routeObject, '\n features: {\n nsfw: true,\n }\n'); + }, + }); + } else if (!nsfwProperty) { + // 有 features 属性但没有 nsfw 属性 + context.report({ + node: featuresProperty.value, + messageId: 'missingNsfwFlag', + fix(fixer) { + const featuresObject = featuresProperty.value; + if (featuresObject.properties.length > 0) { + const lastFeatureProp = featuresObject.properties.at(-1); + return fixer.insertTextAfter(lastFeatureProp, ',\n nsfw: true'); + } + // features 是空对象 + return fixer.replaceTextRange([featuresObject.range[0] + 1, featuresObject.range[1] - 1], '\n nsfw: true,\n '); + }, + }); + } else if (nsfwProperty.value && (nsfwProperty.value.type !== 'Literal' || nsfwProperty.value.value !== true)) { + // nsfw 属性存在但不是 true + context.report({ + node: nsfwProperty.value, + messageId: 'missingNsfwFlag', + fix: (fixer) => fixer.replaceText(nsfwProperty.value, 'true'), + }); } - }, - }; - }, + } + }, + }), }, }, }); diff --git a/flake.lock b/flake.lock index cef7e05c4f14..0c276d68c933 100644 --- a/flake.lock +++ b/flake.lock @@ -64,11 +64,11 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1780316214, - "narHash": "sha256-X3EG0oxt03MegwC/MnQv1saoq9nphEGSSGEAj8mZQOg=", + "lastModified": 1781800860, + "narHash": "sha256-LrEo0eC5ckMvjpBRCuk5q5/vjItKlxnb4n/clHNRZlk=", "owner": "cachix", "repo": "devenv", - "rev": "d5e9138bae90fe199fbe5de7675014d76d28873b", + "rev": "d59d872d80876d9eeb3e214d3b088bc4a14a9c4f", "type": "github" }, "original": { @@ -300,11 +300,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1780243769, - "narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=", + "lastModified": 1781577229, + "narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "331800de5053fcebacf6813adb5db9c9dca22a0c", + "rev": "567a49d1913ce81ac6e9582e3553dd90a955875f", "type": "github" }, "original": { diff --git a/lib/api/category/one.ts b/lib/api/category/one.ts index a8c8726cb9d2..e6f7d4266718 100644 --- a/lib/api/category/one.ts +++ b/lib/api/category/one.ts @@ -9,10 +9,10 @@ for (const namespace in namespaces) { for (const path in namespaces[namespace].routes) { if (namespaces[namespace].routes[path].categories?.length) { for (const category of namespaces[namespace].routes[path].categories!) { - if (!categoryList[category]) { + if (!Object.hasOwn(categoryList, category)) { categoryList[category] = {}; } - if (!categoryList[category][namespace]) { + if (!Object.hasOwn(categoryList[category], namespace)) { categoryList[category][namespace] = { ...namespaces[namespace], routes: {}, diff --git a/lib/api/index.ts b/lib/api/index.ts index 76ba26e49ddd..9ff609b4b234 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -5,7 +5,7 @@ import { Scalar } from '@scalar/hono-api-reference'; import { handler as categoryOneHandler, route as categoryOneRoute } from '@/api/category/one'; import { handler as followConfigHandler, route as followConfigRoute } from '@/api/follow/config'; import { handler as namespaceAllHandler, route as namespaceAllRoute } from '@/api/namespace/all'; -import { handler as namespaceOneHandler, route as namespaceOneRoute } from '@/api/namespace/one'; +import { handler as namespaceOneHandler, route as namespaceOneRoute, routeNested as namespaceOneNestedRoute } from '@/api/namespace/one'; import { handler as radarRulesAllHandler, route as radarRulesAllRoute } from '@/api/radar/rules/all'; import { handler as radarRulesOneHandler, route as radarRulesOneRoute } from '@/api/radar/rules/one'; import { handler as routeStatusHandler, route as routeStatusRoute } from '@/api/route/status'; @@ -14,6 +14,7 @@ const app = new OpenAPIHono(); app.openapi(namespaceAllRoute, namespaceAllHandler); app.openapi(namespaceOneRoute, namespaceOneHandler); +app.openapi(namespaceOneNestedRoute, namespaceOneHandler); app.openapi(radarRulesAllRoute, radarRulesAllHandler); app.openapi(radarRulesOneRoute, radarRulesOneHandler); app.openapi(categoryOneRoute, categoryOneHandler); diff --git a/lib/api/namespace.test.ts b/lib/api/namespace.test.ts index 960e87e1ff8a..d1d318ab2bd5 100644 --- a/lib/api/namespace.test.ts +++ b/lib/api/namespace.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; +import api from '@/api'; import { handler as allHandler } from '@/api/namespace/all'; import { handler as oneHandler } from '@/api/namespace/one'; import { namespaces } from '@/registry'; @@ -13,6 +14,8 @@ const createCtx = (param: Record = {}) => }) as any; describe('api/namespace', () => { + const nestedKey = Object.keys(namespaces).find((key) => key.includes('/')) as string; + it('returns all namespaces', () => { const result = allHandler(createCtx()); expect(result).toBe(namespaces); @@ -22,4 +25,25 @@ describe('api/namespace', () => { const result = oneHandler(createCtx({ namespace: 'test' })); expect(result).toBe(namespaces.test); }); + + it('returns a nested namespace', () => { + expect(nestedKey).toBeDefined(); + const [namespace, sub] = nestedKey.split('/'); + const result = oneHandler(createCtx({ namespace, sub })); + expect(result).toBe(namespaces[nestedKey]); + }); + + it('serves a single namespace over HTTP', async () => { + expect(namespaces.github).toBeDefined(); + const response = await api.request('/namespace/github'); + expect(response.status).toBe(200); + expect(await response.json()).toEqual(namespaces.github); + }); + + it('serves a nested namespace over HTTP', async () => { + expect(nestedKey).toBeDefined(); + const response = await api.request(`/namespace/${nestedKey}`); + expect(response.status).toBe(200); + expect(await response.json()).toEqual(namespaces[nestedKey]); + }); }); diff --git a/lib/api/namespace/one.ts b/lib/api/namespace/one.ts index d447e5805802..9c9252a97a1c 100644 --- a/lib/api/namespace/one.ts +++ b/lib/api/namespace/one.ts @@ -3,15 +3,18 @@ import { createRoute, z } from '@hono/zod-openapi'; import { namespaces } from '@/registry'; -const ParamsSchema = z.object({ - namespace: z.string().openapi({ +const pathParam = (name: string, example: string) => + z.string().openapi({ param: { - name: 'namespace', + name, in: 'path', }, - example: 'github', - }), -}); + example, + }); + +const nestedExample = Object.keys(namespaces) + .find((key) => key.includes('/')) + ?.split('/') ?? ['namespace', 'sub']; const route = createRoute({ method: 'get', @@ -19,7 +22,7 @@ const route = createRoute({ description: 'Information about a namespace', tags: ['Namespace'], request: { - params: ParamsSchema, + params: z.object({ namespace: pathParam('namespace', 'github') }), }, responses: { 200: { @@ -28,9 +31,24 @@ const route = createRoute({ }, }); +const routeNested = createRoute({ + method: 'get', + path: '/namespace/{namespace}/{sub}', + description: `Information about a nested namespace (e.g. ${nestedExample.join('/')})`, + tags: ['Namespace'], + request: { + params: z.object({ namespace: pathParam('namespace', nestedExample[0]), sub: pathParam('sub', nestedExample[1]) }), + }, + responses: { + 200: { + description: 'Namespace registry data for a nested namespace', + }, + }, +}); + const handler: RouteHandler = (ctx) => { - const { namespace } = ctx.req.valid('param'); - return ctx.json(namespaces[namespace]); + const { namespace, sub } = ctx.req.valid('param') as { namespace: string; sub?: string }; + return ctx.json(namespaces[[namespace, sub].filter(Boolean).join('/')]); }; -export { handler, route }; +export { handler, route, routeNested }; diff --git a/lib/api/radar/rules/all.ts b/lib/api/radar/rules/all.ts index d94e925619db..e8136063a787 100644 --- a/lib/api/radar/rules/all.ts +++ b/lib/api/radar/rules/all.ts @@ -19,12 +19,12 @@ for (const namespace in namespaces) { const subdomain = parsedDomain.subdomain || '.'; const domain = parsedDomain.domain; if (domain) { - if (!radar[domain]) { + if (!Object.hasOwn(radar, domain)) { radar[domain] = { _name: namespaces[namespace].name, } as RadarDomain; } - if (!radar[domain][subdomain]) { + if (!Object.hasOwn(radar[domain], subdomain)) { radar[domain][subdomain] = []; } radar[domain][subdomain].push({ diff --git a/lib/api/radar/rules/one.ts b/lib/api/radar/rules/one.ts index 551f3a696ec0..1a28f117b85e 100644 --- a/lib/api/radar/rules/one.ts +++ b/lib/api/radar/rules/one.ts @@ -19,12 +19,12 @@ for (const namespace in namespaces) { const subdomain = parsedDomain.subdomain || '.'; const domain = parsedDomain.domain; if (domain) { - if (!radar[domain]) { + if (!Object.hasOwn(radar, domain)) { radar[domain] = { _name: namespaces[namespace].name, } as RadarDomain; } - if (!radar[domain][subdomain]) { + if (!Object.hasOwn(radar[domain], subdomain)) { radar[domain][subdomain] = []; } radar[domain][subdomain].push({ diff --git a/lib/bilibili-video-route.test.ts b/lib/bilibili-video-route.test.ts index 09f601237bd5..39dd7ea1b034 100644 --- a/lib/bilibili-video-route.test.ts +++ b/lib/bilibili-video-route.test.ts @@ -11,15 +11,13 @@ const destroy = vi.fn(); const getPlaywrightPage = vi.fn(); const goto = vi.fn(); const on = vi.fn(); -const setCookie = vi.fn(); -const setRequestInterception = vi.fn(); +const pageRoute = vi.fn(); const waitForResponse = vi.fn(); const page = { goto, on, - setCookie, - setRequestInterception, + route: pageRoute, waitForResponse, }; @@ -66,8 +64,7 @@ describe('/bilibili/user/video/:uid', () => { getPlaywrightPage.mockReset(); goto.mockReset(); on.mockReset(); - setCookie.mockReset(); - setRequestInterception.mockReset(); + pageRoute.mockReset(); waitForResponse.mockReset(); }); @@ -109,6 +106,7 @@ describe('/bilibili/user/video/:uid', () => { getPlaywrightPage.mockImplementation(async (_url, options) => { await options.onBeforeLoad?.(page); return { + context: {}, destroy, page, }; @@ -171,6 +169,7 @@ describe('/bilibili/user/video/:uid', () => { getPlaywrightPage.mockImplementation(async (_url, options) => { await options.onBeforeLoad?.(page); return { + context: {}, destroy, page, }; diff --git a/lib/config.ts b/lib/config.ts index a460f33723b0..e19ab2b508cb 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -10,6 +10,7 @@ type ConfigEnvKeys = | 'NODE_NAME' | 'PLAYWRIGHT_WS_ENDPOINT' | 'PUPPETEER_WS_ENDPOINT' + | 'PLAYWRIGHT_CDP_ENDPOINT' | 'CHROMIUM_EXECUTABLE_PATH' // Network | 'PORT' @@ -260,6 +261,7 @@ export type Config = { isPackage: boolean; nodeName?: string; playwrightWSEndpoint?: string; + playwrightCDPEndpoint?: string; chromiumExecutablePath?: string; // network connect: { @@ -716,9 +718,8 @@ const TRUE_UA = 'RSSHub/1.0 (+http://github.com/DIYgod/RSSHub; like FeedFetcher- const toBoolean = (value: string | undefined, defaultValue: boolean) => { if (value === undefined) { return defaultValue; - } else { - return value === '' || value === '0' || value === 'false' ? false : !!value; } + return ['', '0', 'false'].includes(value) ? false : !!value; }; const toInt = (value: string | undefined, defaultValue?: number) => (value === undefined ? defaultValue : Number.parseInt(value)); @@ -756,6 +757,7 @@ const calculateValue = () => { isPackage: !!envs.IS_PACKAGE, nodeName: envs.NODE_NAME, playwrightWSEndpoint: envs.PLAYWRIGHT_WS_ENDPOINT ?? envs.PUPPETEER_WS_ENDPOINT, + playwrightCDPEndpoint: envs.PLAYWRIGHT_CDP_ENDPOINT, chromiumExecutablePath: envs.CHROMIUM_EXECUTABLE_PATH, // network connect: { @@ -1217,24 +1219,25 @@ const calculateValue = () => { }; calculateValue(); (async () => { - if (envs.REMOTE_CONFIG) { - const { default: logger } = await import('@/utils/logger'); - try { - const data = await ofetch(envs.REMOTE_CONFIG, { - headers: { - Authorization: `Basic ${envs.REMOTE_CONFIG_AUTH}`, - }, - }); - if (data) { - envs = Object.assign(envs, data); - calculateValue(); - logger.info('Remote config loaded.'); - } else { - logger.error('Remote config load failed.'); - } - } catch (error) { - logger.error('Remote config load failed.', error); + if (!envs.REMOTE_CONFIG) { + return; + } + const { default: logger } = await import('@/utils/logger'); + try { + const data = await ofetch(envs.REMOTE_CONFIG, { + headers: { + Authorization: `Basic ${envs.REMOTE_CONFIG_AUTH}`, + }, + }); + if (data) { + envs = Object.assign(envs, data); + calculateValue(); + logger.info('Remote config loaded.'); + } else { + logger.error('Remote config load failed.'); } + } catch (error) { + logger.error('Remote config load failed.', error); } })(); diff --git a/lib/errors/index.test.ts b/lib/errors/index.test.ts index 9c102b973600..397d227cb769 100644 --- a/lib/errors/index.test.ts +++ b/lib/errors/index.test.ts @@ -25,7 +25,7 @@ describe('httperror', () => { describe('RequestInProgressError', () => { it('RequestInProgressError with retry', async () => { const responses = await Promise.all([app.request('/test/slow'), app.request('/test/slow')]); - expect(new Set(responses.map((r) => r.status))).toEqual(new Set([200, 200])); + expect(responses.map((r) => r.status)).toEqual([200, 200]); }); it('RequestInProgressError', async () => { const responses = await Promise.all([app.request('/test/slow4'), app.request('/test/slow4')]); diff --git a/lib/errors/index.tsx b/lib/errors/index.tsx index b78603815c00..5ad01a336235 100644 --- a/lib/errors/index.tsx +++ b/lib/errors/index.tsx @@ -1,5 +1,4 @@ import Honeybadger from '@honeybadger-io/js'; -import * as Sentry from '@sentry/node'; import type { ErrorHandler, NotFoundHandler } from 'hono'; import { routePath } from 'hono/route'; @@ -11,6 +10,8 @@ import Error from '@/views/error'; import NotFoundError from './types/not-found'; +const Sentry = config.sentry.dsn ? await import('@sentry/node') : undefined; + export const errorHandler: ErrorHandler = (error, ctx) => { const requestPath = ctx.req.path; const matchedRoute = routePath(ctx); @@ -26,12 +27,14 @@ export const errorHandler: ErrorHandler = (error, ctx) => { } debug.error++; - if (!debug.errorPaths[requestPath]) { + const errorPathCount = debug.errorPaths[requestPath]; + if (!errorPathCount) { debug.errorPaths[requestPath] = 0; } debug.errorPaths[requestPath]++; - if (!debug.errorRoutes[matchedRoute] && hasMatchedRoute) { + const errorRouteCount = debug.errorRoutes[matchedRoute]; + if (!errorRouteCount && hasMatchedRoute) { debug.errorRoutes[matchedRoute] = 0; } hasMatchedRoute && debug.errorRoutes[matchedRoute]++; @@ -39,13 +42,13 @@ export const errorHandler: ErrorHandler = (error, ctx) => { if (config.honeybadger.apiKey) { Honeybadger.notify(error, { - context: { name: requestPath.split('/')[1] }, + context: { name: requestPath.split('/', 2)[1] }, }); } - if (config.sentry.dsn) { + if (Sentry) { Sentry.withScope((scope) => { - scope.setTag('name', requestPath.split('/')[1]); + scope.setTag('name', requestPath.split('/', 2)[1]); Sentry.captureException(error); }); } diff --git a/lib/middleware/access-control.ts b/lib/middleware/access-control.ts index 01cbbd5f7b8c..969308227698 100644 --- a/lib/middleware/access-control.ts +++ b/lib/middleware/access-control.ts @@ -13,7 +13,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => { const accessKey = ctx.req.query('key'); const accessCode = ctx.req.query('code'); - if (requestPath === '/' || requestPath === '/robots.txt' || requestPath === '/favicon.ico' || requestPath === '/logo.png') { + if (['/', '/robots.txt', '/favicon.ico', '/logo.png'].includes(requestPath)) { await next(); } else { if (config.accessKey && !(config.accessKey === accessKey || accessCode === md5(requestPath + config.accessKey))) { diff --git a/lib/middleware/anti-hotlink.ts b/lib/middleware/anti-hotlink.ts index 49a849570b04..7dd6e897661d 100644 --- a/lib/middleware/anti-hotlink.ts +++ b/lib/middleware/anti-hotlink.ts @@ -6,7 +6,7 @@ import { config } from '@/config'; import type { Data } from '@/types'; import logger from '@/utils/logger'; -const templateRegex = /\${([^{}]+)}/g; +const templateRegex = /\$\{([^{}]+)\}/g; const allowedUrlProperties = new Set(['hash', 'host', 'hostname', 'href', 'origin', 'password', 'pathname', 'port', 'protocol', 'search', 'searchParams', 'username']); // match path or sub-path @@ -150,7 +150,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => { if (item.enclosure_url && item.enclosure_type) { if (item.enclosure_type.startsWith('image/')) { item.enclosure_url = replaceUrl(imageHotlinkTemplate, item.enclosure_url); - } else if (/^(video|audio)\//.test(item.enclosure_type)) { + } else if (/^(?:video|audio)\//.test(item.enclosure_type)) { item.enclosure_url = replaceUrl(multimediaHotlinkTemplate, item.enclosure_url); } } diff --git a/lib/middleware/debug.ts b/lib/middleware/debug.ts index 9d779728a855..5aca41000dc9 100644 --- a/lib/middleware/debug.ts +++ b/lib/middleware/debug.ts @@ -6,7 +6,8 @@ import { getDebugInfo, setDebugInfo } from '@/utils/debug-info'; const middleware: MiddlewareHandler = async (ctx, next) => { { const debug = getDebugInfo(); - if (!debug.paths[ctx.req.path]) { + const pathCount = debug.paths[ctx.req.path]; + if (!pathCount) { debug.paths[ctx.req.path] = 0; } debug.paths[ctx.req.path]++; @@ -21,7 +22,8 @@ const middleware: MiddlewareHandler = async (ctx, next) => { const debug = getDebugInfo(); const rPath = routePath(ctx); const hasMatchedRoute = rPath !== '/*'; - if (!debug.routes[rPath] && hasMatchedRoute) { + const routeCount = debug.routes[rPath]; + if (!routeCount && hasMatchedRoute) { debug.routes[rPath] = 0; } hasMatchedRoute && debug.routes[rPath]++; diff --git a/lib/middleware/parameter.ts b/lib/middleware/parameter.ts index 07e70e098005..ea5e81da8159 100644 --- a/lib/middleware/parameter.ts +++ b/lib/middleware/parameter.ts @@ -264,7 +264,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => { } if (ctx.req.query('filterout_category')) { const categoryRegex = makeRegex(ctx.req.query('filterout_category')!); - isFilter = isFilter && !category.some((c) => (categoryRegex instanceof RE2JS ? categoryRegex.matcher(c).find() : c.match(categoryRegex))); + isFilter = isFilter && category.every((c) => !(categoryRegex instanceof RE2JS ? categoryRegex.matcher(c).find() : c.match(categoryRegex))); } return isFilter; @@ -296,9 +296,8 @@ const middleware: MiddlewareHandler = async (ctx, next) => { const encodedlink = encodeURIComponent(item.link); item.link = `https://t.me/iv?url=${encodedlink}&rhash=${ctx.req.query('tgiv')}`; return item; - } else { - return item; } + return item; }); } @@ -407,9 +406,8 @@ const middleware: MiddlewareHandler = async (ctx, next) => { if (num.test(ctx.req.query('brief')!)) { const brief: number = Number.parseInt(ctx.req.query('brief')!); for (const item of data.item) { - let text; if (item.description) { - text = sanitizeHtml(item.description, { allowedTags: [], allowedAttributes: {} }); + const text = sanitizeHtml(item.description, { allowedTags: [], allowedAttributes: {} }); item.description = text.length > brief ? `

${text.slice(0, brief)}…

` : `

${text}

`; } } diff --git a/lib/middleware/sentry.test.ts b/lib/middleware/sentry.test.ts index 50c88bac0050..f9a5fd55b5ca 100644 --- a/lib/middleware/sentry.test.ts +++ b/lib/middleware/sentry.test.ts @@ -47,6 +47,25 @@ describe('sentry middleware', () => { return { middleware, sentry, logger, scope, getRouteNameFromPath }; }; + it('does not load sentry when dsn is not configured', async () => { + const sentryFactory = vi.fn(() => ({ init: vi.fn() })); + vi.doMock('@sentry/node', sentryFactory); + vi.doMock('@/config', () => ({ + config: { + sentry: { + dsn: '', + }, + errorTrackingRouteTimeout: 50, + nodeName: 'node-a', + }, + })); + + const { default: middleware } = await import('@/middleware/sentry'); + await middleware({ req: { path: '/test/slow' } } as any, async () => {}); + + expect(sentryFactory).not.toHaveBeenCalled(); + }); + it('initializes sentry and captures slow routes', async () => { const { middleware, sentry, logger, scope, getRouteNameFromPath } = await loadMiddleware(); diff --git a/lib/middleware/sentry.ts b/lib/middleware/sentry.ts index efc6f1679e7b..de34a9217359 100644 --- a/lib/middleware/sentry.ts +++ b/lib/middleware/sentry.ts @@ -1,11 +1,14 @@ -import * as Sentry from '@sentry/node'; +import type * as SentryType from '@sentry/node'; import type { MiddlewareHandler } from 'hono'; import { config } from '@/config'; import { getRouteNameFromPath } from '@/utils/helpers'; import logger from '@/utils/logger'; +let Sentry: typeof SentryType | undefined; + if (config.sentry.dsn) { + Sentry = await import('@sentry/node'); Sentry.init({ dsn: config.sentry.dsn, }); @@ -17,10 +20,10 @@ if (config.sentry.dsn) { const middleware: MiddlewareHandler = async (ctx, next) => { const time = Date.now(); await next(); - if (config.sentry.dsn && Date.now() - time >= config.errorTrackingRouteTimeout) { + if (Sentry && Date.now() - time >= config.errorTrackingRouteTimeout) { Sentry.withScope((scope) => { scope.setTag('name', getRouteNameFromPath(ctx.req.path)); - Sentry.captureException(new Error('Route Timeout')); + Sentry!.captureException(new Error('Route Timeout')); }); } }; diff --git a/lib/middleware/template.tsx b/lib/middleware/template.tsx index d9f5ed98655c..1b3038ad39ad 100644 --- a/lib/middleware/template.tsx +++ b/lib/middleware/template.tsx @@ -28,7 +28,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => { return ctx.json(ctx.get('json') || { message: 'plugin does not set debug json' }); } - if (/(\d+)\.debug\.html$/.test(outputType)) { + if (/\d+\.debug\.html$/.test(outputType)) { const index = Number.parseInt(outputType.match(/(\d+)\.debug\.html$/)?.[1] || '0'); return ctx.html(data?.item?.[index]?.description || `data.item[${index}].description not found`); } @@ -58,7 +58,8 @@ const middleware: MiddlewareHandler = async (ctx, next) => { // https://stackoverflow.com/questions/1497885/remove-control-characters-from-php-string/1497928#1497928 // remove unicode control characters // see #14940 #14943 #15262 - item.description = item.description.replaceAll(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F\u200B\uFFFF]/g, ''); + // oxlint-disable-next-line no-control-regex + item.description = item.description.replaceAll(/[\u0000-\u0009\v\f\u000E-\u001F\u007F\u200B\uFFFF]/g, ''); } if (typeof item.author === 'string') { @@ -109,22 +110,22 @@ const middleware: MiddlewareHandler = async (ctx, next) => { if (ctx.get('redirect')) { return ctx.redirect(ctx.get('redirect'), 301); - } else if (ctx.get('no-content')) { + } + if (ctx.get('no-content')) { return ctx.body(null); - } else { - // retain .ums for backward compatibility - switch (outputType) { - case 'ums': - case 'rss3': - return ctx.json(rss3(result)); - case 'json': - ctx.header('Content-Type', 'application/feed+json; charset=UTF-8'); - return ctx.body(json(result)); - case 'atom': - return ctx.render(); - default: - return ctx.render(); - } + } + // retain .ums for backward compatibility + switch (outputType) { + case 'ums': + case 'rss3': + return ctx.json(rss3(result)); + case 'json': + ctx.header('Content-Type', 'application/feed+json; charset=UTF-8'); + return ctx.body(json(result)); + case 'atom': + return ctx.render(); + default: + return ctx.render(); } }; diff --git a/lib/pkg.ts b/lib/pkg.ts index 37762522c935..6ce9cf5f42b0 100644 --- a/lib/pkg.ts +++ b/lib/pkg.ts @@ -42,7 +42,7 @@ export async function registerRoute(namespace: string, route: Route, namespaceCo const { namespaces } = await import('./registry'); - if (!namespaces[namespace]) { + if (!Object.hasOwn(namespaces, namespace)) { namespaces[namespace] = { ...namespaceConfig, name: namespaceConfig?.name || namespace, @@ -55,13 +55,14 @@ export async function registerRoute(namespace: string, route: Route, namespaceCo const subApp = app.basePath(`/${namespace}`); const wrappedHandler: Handler = async (ctx) => { - if (!ctx.get('data')) { - const response = await route.handler(ctx); - if (response instanceof Response) { - return response; - } - ctx.set('data', response); + if (ctx.get('data')) { + return; } + const response = await route.handler(ctx); + if (response instanceof Response) { + return response; + } + ctx.set('data', response); }; for (const path of paths) { diff --git a/lib/registry.test.ts b/lib/registry.test.ts index dfbdde572c8d..27d691911a15 100644 --- a/lib/registry.test.ts +++ b/lib/registry.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it, vi } from 'vitest'; import app from '@/app'; import { config } from '@/config'; +import registryApp, { collectNamespaceRoots, namespaces, resolveModuleNamespace, sortRoutes } from '@/registry'; +import type { Route } from '@/types'; describe('registry', () => { // root @@ -68,3 +70,70 @@ describe('registry', () => { expect(namespaces['2048']).toBeDefined(); }); }); + +describe('namespace-resolution', () => { + const keys = [ + '/github/namespace.ts', + '/github/issue.ts', + '/example/sub/namespace.ts', + '/example/sub/news.ts', + '/example/flat/typical.ts', + '/example/nested/namespace.ts', + '/example/nested/zzb.ts', + '/example/nested/deep/namespace.ts', + '/example/nested/deep/zwfw.ts', + ]; + const roots = collectNamespaceRoots(keys); + + it('collects directories containing namespace.ts', () => { + expect(roots).toEqual(new Set(['github', 'example/sub', 'example/nested', 'example/nested/deep'])); + }); + + it('resolves a flat module to its top directory', () => { + expect(resolveModuleNamespace('/github/issue.ts', roots)).toEqual({ namespace: 'github', location: 'issue.ts' }); + }); + + it('resolves a nested module to its namespace root', () => { + expect(resolveModuleNamespace('/example/sub/news.ts', roots)).toEqual({ namespace: 'example/sub', location: 'news.ts' }); + }); + + it('prefers the deepest matching root', () => { + expect(resolveModuleNamespace('/example/nested/deep/zwfw.ts', roots)).toEqual({ namespace: 'example/nested/deep', location: 'zwfw.ts' }); + expect(resolveModuleNamespace('/example/nested/zzb.ts', roots)).toEqual({ namespace: 'example/nested', location: 'zzb.ts' }); + }); + + it('falls back to the first segment when no root matches', () => { + expect(resolveModuleNamespace('/example/flat/typical.ts', roots)).toEqual({ namespace: 'example', location: 'flat/typical.ts' }); + }); + + it('resolves namespace.ts itself to its own root', () => { + expect(resolveModuleNamespace('/example/sub/namespace.ts', roots)).toEqual({ namespace: 'example/sub', location: 'namespace.ts' }); + }); +}); + +describe('nested namespace mounting', () => { + it('registers deeper namespaces before shallower ones', () => { + const keys = Object.keys(namespaces).filter((key) => Object.keys(namespaces[key].routes ?? {}).length > 0); + const byDepth = keys.toSorted((a, b) => b.split('/').length - a.split('/').length); + const deep = byDepth[0]; + const shallow = byDepth.at(-1) as string; + expect(deep.split('/').length).toBeGreaterThan(shallow.split('/').length); + + const paths = registryApp.routes.map((r) => r.path); + const deepIndex = paths.indexOf(`/${deep}${Object.keys(namespaces[deep].routes)[0]}`); + const shallowIndex = paths.indexOf(`/${shallow}${Object.keys(namespaces[shallow].routes)[0]}`); + expect(deepIndex).toBeGreaterThanOrEqual(0); + expect(shallowIndex).toBeGreaterThanOrEqual(0); + expect(deepIndex).toBeLessThan(shallowIndex); + }); + + it('sorts regex-constrained params before plain params', () => { + const stub = {} as Route & { location: string }; + const sorted = sortRoutes({ + '/:category?': stub, + '/:id{[0-9]+}': stub, + '/static': stub, + }); + expect(sorted.map(([path]) => path)).toEqual(['/static', '/:id{[0-9]+}', '/:category?']); + }); +}); diff --git a/lib/registry.ts b/lib/registry.ts index 414dfb5d15db..ec12e10a400b 100644 --- a/lib/registry.ts +++ b/lib/registry.ts @@ -17,6 +17,38 @@ import logger from '@/utils/logger'; const __dirname = import.meta.dirname; +const SEPARATOR = /[/\\]/; + +/** + * A directory under lib/routes that contains a `namespace.ts` is a "namespace root". Roots are keyed by their path + * relative to lib/routes, joined with `/` (e.g. `gov/cn`). + */ +export function collectNamespaceRoots(moduleKeys: string[]): Set { + const roots = new Set(); + for (const key of moduleKeys) { + const segments = key.split(SEPARATOR).filter(Boolean); + if (segments.at(-1) === 'namespace.ts') { + roots.add(segments.slice(0, -1).join('/')); + } + } + return roots; +} + +/** + * A module belongs to its longest matching namespace root; modules with no matching root fall back to their first + * path segment (flat namespaces). `location` is the module path relative to the resolved namespace root. + */ +export function resolveModuleNamespace(moduleKey: string, roots: Set): { namespace: string; location: string } { + const segments = moduleKey.split(SEPARATOR).filter(Boolean); + for (let i = segments.length - 1; i >= 1; i--) { + const candidate = segments.slice(0, i).join('/'); + if (roots.has(candidate)) { + return { namespace: candidate, location: segments.slice(i).join('/') }; + } + } + return { namespace: segments[0], location: segments.slice(1).join('/') }; +} + function isSafeRoutes(routes: RoutesType): boolean { return Object.values(routes).every((route: Route) => !route.features?.nsfw); } @@ -84,6 +116,7 @@ if (config.feature.disable_nsfw) { } if (Object.keys(modules).length) { + const namespaceRoots = collectNamespaceRoots(Object.keys(modules)); for (const module in modules) { const content = modules[module] as | { @@ -95,7 +128,7 @@ if (Object.keys(modules).length) { | { apiRoute: APIRoute; }; - const namespace = module.split(/[/\\]/)[1]; + const { namespace, location } = resolveModuleNamespace(module, namespaceRoots); if ('namespace' in content) { namespaces[namespace] = Object.assign( { @@ -106,7 +139,7 @@ if (Object.keys(modules).length) { content.namespace ); } else if ('route' in content) { - if (!namespaces[namespace]) { + if (!Object.hasOwn(namespaces, namespace)) { namespaces[namespace] = { name: namespace, routes: {}, @@ -117,17 +150,17 @@ 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, }; } } else { namespaces[namespace].routes[content.route.path] = { ...content.route, - location: module.split(/[/\\]/).slice(2).join('/'), + location, }; } } else if ('apiRoute' in content) { - if (!namespaces[namespace]) { + if (!Object.hasOwn(namespaces, namespace)) { namespaces[namespace] = { name: namespace, routes: {}, @@ -138,13 +171,13 @@ if (Object.keys(modules).length) { for (const path of content.apiRoute.path) { namespaces[namespace].apiRoutes[path] = { ...content.apiRoute, - location: module.split(/[/\\]/).slice(2).join('/'), + location, }; } } else { namespaces[namespace].apiRoutes[content.apiRoute.path] = { ...content.apiRoute, - location: module.split(/[/\\]/).slice(2).join('/'), + location, }; } } @@ -154,7 +187,7 @@ if (Object.keys(modules).length) { export { namespaces }; const app = new Hono(); -const sortRoutes = ( +export const sortRoutes = ( routes: Record< string, Route & { @@ -178,12 +211,20 @@ const sortRoutes = ( if (segmentA.startsWith(':') !== segmentB.startsWith(':')) { return segmentA.startsWith(':') ? 1 : -1; } + + // Regex-constrained parameters have priority over plain parameters + if (segmentA.startsWith(':') && segmentA.includes('{') !== segmentB.includes('{')) { + return segmentA.includes('{') ? -1 : 1; + } } return 0; }); -for (const namespace in namespaces) { +// Deeper namespaces register first so a parent's param routes cannot shadow them +const namespacesByDepth = Object.keys(namespaces).toSorted((a, b) => b.split('/').length - a.split('/').length); + +for (const namespace of namespacesByDepth) { const subApp = app.basePath(`/${namespace}`); const namespaceData = namespaces[namespace]; @@ -217,7 +258,7 @@ for (const namespace in namespaces) { } } -for (const namespace in namespaces) { +for (const namespace of namespacesByDepth) { const subApp = app.basePath(`/api/${namespace}`); const namespaceData = namespaces[namespace]; @@ -237,19 +278,20 @@ for (const namespace in namespaces) { for (const [path, routeData] of sortedRoutes) { const wrappedHandler: Handler = async (ctx) => { - if (!ctx.get('apiData')) { - if (typeof routeData.handler !== 'function') { - if (process.env.NODE_ENV === 'test') { - const { apiRoute } = await import(`./routes/${namespace}/${routeData.location}`); - routeData.handler = apiRoute.handler; - } else if (routeData.module) { - const { apiRoute } = await routeData.module(); - routeData.handler = apiRoute.handler; - } + if (ctx.get('apiData')) { + return; + } + if (typeof routeData.handler !== 'function') { + if (process.env.NODE_ENV === 'test') { + const { apiRoute } = await import(`./routes/${namespace}/${routeData.location}`); + routeData.handler = apiRoute.handler; + } else if (routeData.module) { + const { apiRoute } = await routeData.module(); + routeData.handler = apiRoute.handler; } - const data = await routeData.handler(ctx); - ctx.set('apiData', data); } + const data = await routeData.handler(ctx); + ctx.set('apiData', data); }; subApp.get(path, wrappedHandler); } diff --git a/lib/routes/005/index.tsx b/lib/routes/005/index.tsx index 43c2be4b5b92..166d7301520e 100644 --- a/lib/routes/005/index.tsx +++ b/lib/routes/005/index.tsx @@ -9,7 +9,7 @@ import timezone from '@/utils/timezone'; 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 limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const rootUrl = 'https://005.tv'; const currentUrl = new URL(category ? `${category}/` : '', rootUrl).href; @@ -85,7 +85,7 @@ export const handler = async (ctx) => { return { title, - description: title.split(/_/)[0], + description: title.split(/_/, 1)[0], link: currentUrl, item: items, allowEmpty: true, diff --git a/lib/routes/0xxx/index.ts b/lib/routes/0xxx/index.ts index 78ff90a68380..bfe02481d7d9 100644 --- a/lib/routes/0xxx/index.ts +++ b/lib/routes/0xxx/index.ts @@ -13,7 +13,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { filter } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '100', 10); + const limit = Number(ctx.req.query('limit') ?? '100'); const baseUrl = 'https://0xxx.ws'; const targetUrl: string = new URL(filter ? `?${filter}` : '', baseUrl).href; diff --git a/lib/routes/10000link/info.ts b/lib/routes/10000link/info.ts index 3b7adf2caf75..cbf4e69b72b0 100644 --- a/lib/routes/10000link/info.ts +++ b/lib/routes/10000link/info.ts @@ -13,7 +13,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { category = 'newslists', id } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://info.10000link.com'; const targetUrl: string = new URL(`${category}.aspx${id ? `?chid=${id}` : ''}`, baseUrl).href; diff --git a/lib/routes/10jqka/realtimenews.ts b/lib/routes/10jqka/realtimenews.ts index 684217bfd7fb..6b8742133abd 100644 --- a/lib/routes/10jqka/realtimenews.ts +++ b/lib/routes/10jqka/realtimenews.ts @@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx) => { const { tag } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const rootUrl = 'https://news.10jqka.com.cn'; const apiUrl = new URL('tapp/news/push/stock', rootUrl).href; diff --git a/lib/routes/121/weather-live.tsx b/lib/routes/121/weather-live.tsx index 63e586781810..bc7b70de8510 100644 --- a/lib/routes/121/weather-live.tsx +++ b/lib/routes/121/weather-live.tsx @@ -25,7 +25,7 @@ const renderDescription = (description, images) => ); export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '100', 10); + const limit = Number(ctx.req.query('limit') ?? '100'); const baseUrl = 'https://tf.121.com.cn'; const imgBaseUrl = 'https://wx.121.com.cn'; diff --git a/lib/routes/12371/zxfb.ts b/lib/routes/12371/zxfb.ts index e5049220597f..ff1ac554abe5 100644 --- a/lib/routes/12371/zxfb.ts +++ b/lib/routes/12371/zxfb.ts @@ -8,7 +8,7 @@ import timezone from '@/utils/timezone'; const handler = async (ctx) => { const { category = 'zxfb' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const rootUrl = 'https://www.12371.cn/'; const currentUrl = `${rootUrl}${category}/`; @@ -16,7 +16,7 @@ const handler = async (ctx) => { const $ = cheerio.load(response.data); - const pattern = /item=(\[{.*?}]);/; + const pattern = /item=(\[\{.*?\}\]);/; const newsList = JSON.parse($('script[language="javascript"]').text().match(pattern)?.[1].replaceAll("'", '"') || '[]'); const topNewsList = newsList.slice(0, limit).map((item) => ({ diff --git a/lib/routes/141jav/index.tsx b/lib/routes/141jav/index.tsx index 350aa2b25cce..03818ae3fecb 100644 --- a/lib/routes/141jav/index.tsx +++ b/lib/routes/141jav/index.tsx @@ -7,7 +7,7 @@ import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: '/:type/:keyword{.*}?', + path: '/:type/:keyword{.+}?', categories: ['multimedia'], name: '通用', maintainers: ['cgkings', 'nczitzk'], @@ -104,7 +104,7 @@ async function handler(ctx) { }); return { - title: `141JAV - ${$('title').text().split('-')[0].trim()}`, + title: `141JAV - ${$('title').text().split('-', 1)[0].trim()}`, link: currentUrl, item: items, }; diff --git a/lib/routes/141ppv/index.tsx b/lib/routes/141ppv/index.tsx index 4d27b39b20c3..4cd480a5396c 100644 --- a/lib/routes/141ppv/index.tsx +++ b/lib/routes/141ppv/index.tsx @@ -7,7 +7,7 @@ import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: '/:type/:keyword{.*}?', + path: '/:type/:keyword{.+}?', categories: ['multimedia'], name: '通用', maintainers: ['cgkings', 'nczitzk'], @@ -163,7 +163,7 @@ async function handler(ctx) { }); return { - title: `141PPV - ${$('title').text().split('-')[0].trim()}`, + title: `141PPV - ${$('title').text().split('-', 1)[0].trim()}`, link: currentUrl, item: items, }; diff --git a/lib/routes/163/music/artist.ts b/lib/routes/163/music/artist.ts index a555357f4390..4f4b6edeccb5 100644 --- a/lib/routes/163/music/artist.ts +++ b/lib/routes/163/music/artist.ts @@ -38,7 +38,10 @@ async function handler(ctx) { description: `网易云音乐歌手专辑 - ${data.artist.name}`, image: data.artist.img1v1Url || data.artist.picUrl, item: data.hotAlbums.map((item) => { - const singer = item.artists.length === 1 ? item.artists[0].name : item.artists.reduce((prev, cur) => (prev.name || prev) + '/' + cur.name); + let singer = item.artists[0].name; + for (const artist of item.artists.slice(1)) { + singer += '/' + artist.name; + } return { title: `${item.name} - ${singer}`, description: renderPlaylistDescription({ diff --git a/lib/routes/163/music/playlist.ts b/lib/routes/163/music/playlist.ts index 8585b120194d..173b3b308b93 100644 --- a/lib/routes/163/music/playlist.ts +++ b/lib/routes/163/music/playlist.ts @@ -55,7 +55,10 @@ async function handler(ctx) { description: `网易云音乐歌单 - ${data.name}`, item: data.trackIds.slice(0, 201).map((item) => { const thisSong = songs.find((element) => element.id === item.id); - const singer = thisSong.artists.length === 1 ? thisSong.artists[0].name : thisSong.artists.reduce((prev, cur) => (prev.name || prev) + '/' + cur.name); + let singer = thisSong.artists[0].name; + for (const artist of thisSong.artists.slice(1)) { + singer += '/' + artist.name; + } return { title: `${thisSong.name} - ${singer}`, description: renderPlaylistDescription({ diff --git a/lib/routes/163/news/rank.ts b/lib/routes/163/news/rank.ts index d7643a8d3aa0..8cd03a5bf55e 100644 --- a/lib/routes/163/news/rank.ts +++ b/lib/routes/163/news/rank.ts @@ -122,7 +122,8 @@ async function handler(ctx) { const cfg = config[category]; if (!cfg) { throw new InvalidParameterError('Bad category. See docs'); - } else if ((category !== 'whole' && type === 'click' && time === 'month') || (category === 'whole' && type === 'click' && time === 'hour') || (type === 'follow' && time === 'hour')) { + } + if ((category !== 'whole' && type === 'click' && time === 'month') || (category === 'whole' && type === 'click' && time === 'hour') || (type === 'follow' && time === 'hour')) { throw new InvalidParameterError('Bad timeRange range. See docs'); } @@ -152,8 +153,8 @@ async function handler(ctx) { cache.tryGet(item.link, async () => { try { let link; - if (category === 'auto' || category === 'house' || category === 'travel') { - const category = item.link.split('.163.com')[0].split('//').pop().split('.').pop(); + if (['auto', 'house', 'travel'].includes(category)) { + const category = item.link.split('.163.com', 1)[0].split('//').pop().split('.').pop(); link = `https://3g.163.com/${category}/article/${item.link.split('/').pop()}`; } else { const pathname = new URL(item.link).pathname; diff --git a/lib/routes/163/news/special.ts b/lib/routes/163/news/special.ts index ea7d1948c278..86f93caba8df 100644 --- a/lib/routes/163/news/special.ts +++ b/lib/routes/163/news/special.ts @@ -102,7 +102,7 @@ async function handler(ctx) { const url = `https://3g.163.com/touch/reconstruct/article/list/${type}/0-20.html`; const response = await got(url); const data = response.data; - const matches = data.replaceAll(/\s/g, '').match(/artiList\((.*?)]}\)/); + const matches = data.replaceAll(/\s/g, '').match(/artiList\((.*?)\]\}\)/); const articlelist0 = matches[1].replace(/".*?wangning/, '"articles') + ']}'; const articlelist = JSON.parse(articlelist0); const articles = articlelist.articles; @@ -112,7 +112,7 @@ async function handler(ctx) { let url = article.url; if (url === null || article.skipType === 'video') { const skipurl = article.skipURL; - const vid = skipurl.match(/vid=(.*?)$/); + const vid = skipurl.match(/vid=(.*)$/); if (vid !== null) { url = `https://3g.163.com/exclusive/video/${vid[1]}.html`; } diff --git a/lib/routes/163/open/vip.tsx b/lib/routes/163/open/vip.tsx index 4ee699dbc0dd..82784e98fb50 100644 --- a/lib/routes/163/open/vip.tsx +++ b/lib/routes/163/open/vip.tsx @@ -65,7 +65,7 @@ async function handler() { const initialState = JSON.parse( $('script') .text() - .match(/window\.__INITIAL_STATE__=(.*);\(function\(\){var/)[1] + .match(/window\.__INITIAL_STATE__=(.*);\(function\(\)\{var/)[1] ); const list = Object.values(initialState.courseindex.myModules).flatMap((mod) => @@ -93,7 +93,7 @@ async function handler() { const $ = load(data.courseInfo.description, null, false); $('img').each((_, img) => { - img.attribs.src = img.attribs.src.split('?')[0]; + img.attribs.src = img.attribs.src.split('?', 1)[0]; delete img.attribs.width; }); diff --git a/lib/routes/163/renjian.ts b/lib/routes/163/renjian.ts index c1f92c510a8d..d3d066ac67f1 100644 --- a/lib/routes/163/renjian.ts +++ b/lib/routes/163/renjian.ts @@ -91,7 +91,7 @@ async function handler(ctx) { .text() .match(/renjian_author = '(.*)'/)[1]; item.description = content('#endText').html() ?? content('#content').html(); - item.pubDate = timezone(parseDate(content('.pub_time').text() ?? content('.post_info').text().split('来源:')[0].trim()), 8); + item.pubDate = timezone(parseDate(content('.pub_time').text() ?? content('.post_info').text().split('来源:', 1)[0].trim()), 8); return item; }) diff --git a/lib/routes/18comic/blogs.ts b/lib/routes/18comic/blogs.ts index 0f127d966982..65399a95c3dd 100644 --- a/lib/routes/18comic/blogs.ts +++ b/lib/routes/18comic/blogs.ts @@ -91,7 +91,7 @@ async function handler(ctx) { return { title: $('title') .text() - .replace(/最新的/, $('.article-nav .active').text()), + .replace(/最新的/, () => $('.article-nav .active').text()), link: currentUrl, item: items, description: $('meta[property="og:description"]').attr('content'), diff --git a/lib/routes/199it/index.tsx b/lib/routes/199it/index.tsx index 39d540fd3a32..b1d7ac29be1a 100644 --- a/lib/routes/199it/index.tsx +++ b/lib/routes/199it/index.tsx @@ -12,7 +12,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const { category = 'newly' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://www.199it.com'; const targetUrl: string = new URL(category, baseUrl).href; diff --git a/lib/routes/19lou/index.ts b/lib/routes/19lou/index.ts index 579552d48eb4..fb5be0179254 100644 --- a/lib/routes/19lou/index.ts +++ b/lib/routes/19lou/index.ts @@ -9,7 +9,7 @@ import { parseDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; import { isValidHost } from '@/utils/valid-host'; -const setCookie = function (cookieName, cookieValue, seconds, path, domain, secure) { +const setCookie = function (cookieName, cookieValue, seconds, path, domain, secure?) { let expires = null; if (seconds !== -1) { expires = new Date(); @@ -108,7 +108,7 @@ async function handler(ctx) { ); return { - title: $('title').text().split('-')[0], + title: $('title').text().split('-', 1)[0], link: rootUrl, item: items, }; diff --git a/lib/routes/1lou/index.ts b/lib/routes/1lou/index.ts index 88bfc90e82a5..abc54f06b29d 100644 --- a/lib/routes/1lou/index.ts +++ b/lib/routes/1lou/index.ts @@ -10,7 +10,7 @@ const rootUrl = 'https://www.1lou.me'; export const handler = async (ctx) => { const { params } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 50; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 50; const queryString = Object.entries(ctx.req.query()) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) @@ -95,7 +95,7 @@ export const handler = async (ctx) => { const image = new URL($('img.logo-2').prop('src'), rootUrl).href; return { - title: `${$('title').text().split(/-/)[0]} - ${author}`, + title: `${$('title').text().split(/-/, 1)[0]} - ${author}`, description: $('meta[name="description"]').prop('content'), link: currentUrl, item: items, diff --git a/lib/routes/1x/index.tsx b/lib/routes/1x/index.tsx index 83e0f52d2ebb..b89d21b67934 100644 --- a/lib/routes/1x/index.tsx +++ b/lib/routes/1x/index.tsx @@ -7,7 +7,7 @@ import got from '@/utils/got'; 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 limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const rootUrl = 'https://1x.com'; const currentUrl = new URL(`gallery/${category}`, rootUrl).href; diff --git a/lib/routes/2048/index.tsx b/lib/routes/2048/index.tsx index 46c1e53563c2..4cf967857835 100644 --- a/lib/routes/2048/index.tsx +++ b/lib/routes/2048/index.tsx @@ -164,7 +164,7 @@ async function handler(ctx) { } } if (!item.enclosure_url) { - const hashMatch = readTpcHtml.match(/哈希校验[^;]*;\s*([a-fA-F0-9]{40})\s*[;;]/); + const hashMatch = readTpcHtml.match(/哈希校验[^;]*;\s*([a-f0-9]{40})\s*[;;]/i); const magnetFromHash = hashMatch ? `magnet:?xt=urn:btih:${hashMatch[1]}` : null; const magnetFromText = magnetText.match(/magnet:\?xt=urn:btih:[^\s"'<>]+/)?.[0]; const magnetLink = magnetFromText ?? readTpcHtml.match(/magnet:\?xt=urn:btih:[^\s"'<>]+/)?.[0] ?? magnetFromHash ?? copyLink; diff --git a/lib/routes/21caijing/channel.ts b/lib/routes/21caijing/channel.ts index 0ec8a3dc6a2d..1cecf429ab39 100644 --- a/lib/routes/21caijing/channel.ts +++ b/lib/routes/21caijing/channel.ts @@ -43,7 +43,7 @@ const processMenu = (data: any[]) => { export const handler = async (ctx: Context): Promise => { const { name = '热点' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const domain = 'm.21jingji.com'; const baseUrl = `https://${domain}`; diff --git a/lib/routes/30secondsofcode/category.ts b/lib/routes/30secondsofcode/category.ts index 71baa0a42549..cd19f4b79624 100644 --- a/lib/routes/30secondsofcode/category.ts +++ b/lib/routes/30secondsofcode/category.ts @@ -8,7 +8,7 @@ import { processList } from './utils'; export const route: Route = { path: '/category/:category?/:subCategory?', categories: ['programming'], - example: '/category/css/interactivity', + example: '/30secondsofcode/category/css/interactivity', parameters: { category: { description: 'Main Category. For Complete list visit site "https://www.30secondsofcode.org/collections/p/1/"', diff --git a/lib/routes/30secondsofcode/new-and-popular.ts b/lib/routes/30secondsofcode/new-and-popular.ts index 1ab63ca40d90..44896648ebb0 100644 --- a/lib/routes/30secondsofcode/new-and-popular.ts +++ b/lib/routes/30secondsofcode/new-and-popular.ts @@ -8,7 +8,7 @@ import { processList, rootUrl } from './utils'; export const route: Route = { path: '/latest', categories: ['programming'], - example: '/latest', + example: '/30secondsofcode/latest', features: { requireConfig: false, requirePuppeteer: false, diff --git a/lib/routes/36kr/hot-list.ts b/lib/routes/36kr/hot-list.ts index c28b4f287e81..3c48333cb498 100644 --- a/lib/routes/36kr/hot-list.ts +++ b/lib/routes/36kr/hot-list.ts @@ -64,7 +64,7 @@ const getProperty = (object, key) => { async function handler(ctx) { const category = ctx.req.param('category') ?? '24'; - if (!categories[category]) { + if (!Object.hasOwn(categories, category)) { throw new InvalidParameterError('This category does not exist. Please refer to the documentation for the correct usage.'); } @@ -79,7 +79,7 @@ async function handler(ctx) { }, }); - const data = getProperty(JSON.parse(response.data.match(/window.initialState=({.*})/)[1]), categories[category].key); + const data = getProperty(JSON.parse(response.data.match(/window.initialState=(\{.*\})/)[1]), categories[category].key); let items = data .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10) diff --git a/lib/routes/36kr/index.ts b/lib/routes/36kr/index.ts index 6c24a1d83726..d5790121a9a2 100644 --- a/lib/routes/36kr/index.ts +++ b/lib/routes/36kr/index.ts @@ -48,7 +48,7 @@ async function handler(ctx) { const $ = load(response.data); - const data = JSON.parse(response.data.match(/"itemList":(\[.*?])/)[1]); + const data = JSON.parse(response.data.match(/"itemList":(\[.*?\])/)[1]); let items = data .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 30) @@ -64,12 +64,12 @@ async function handler(ctx) { }; }); - if (!/^\/(search|newsflashes)/.test(path)) { + if (!/^\/(?:search|newsflashes)/.test(path)) { items = await Promise.all(items.map((item) => ProcessItem(item, cache.tryGet))); } return { - title: `36氪 - ${$('title').text().split('_')[0]}`, + title: `36氪 - ${$('title').text().split('_', 1)[0]}`, link: currentUrl, item: items, }; diff --git a/lib/routes/36kr/utils.ts b/lib/routes/36kr/utils.ts index 5149e1ae2864..e20b6b7e6cc0 100644 --- a/lib/routes/36kr/utils.ts +++ b/lib/routes/36kr/utils.ts @@ -11,7 +11,7 @@ export const ProcessItem = (item, tryGet) => tryGet(item.link, async () => { const detailResponse = await ofetch(item.link); - const cipherTextList = detailResponse.match(/{"state":"(.*)","isEncrypt":true}/) ?? []; + const cipherTextList = detailResponse.match(/\{"state":"(.*)","isEncrypt":true\}/) ?? []; if (cipherTextList.length === 0) { const $ = load(detailResponse); @@ -54,8 +54,8 @@ export const getWafTokenId = () => const _wafTokenId = tokenIdResponse.headers .getSetCookie() .find((cookie) => cookie.startsWith('_waftokenid=')) - ?.split(';')[0] - .split('=')[1]; + ?.split(';', 1)[0] + .split('=', 2)[1]; return _wafTokenId as string; }, diff --git a/lib/routes/3dmgame/game.ts b/lib/routes/3dmgame/game.ts index 888ce02f46be..9904396f059a 100644 --- a/lib/routes/3dmgame/game.ts +++ b/lib/routes/3dmgame/game.ts @@ -43,7 +43,7 @@ async function handler(ctx) { const items = await Promise.all(list.map((item) => parseArticle(item, cache.tryGet))); return { - title: $('head title').text().split('_')[0], + title: $('head title').text().split('_', 1)[0], description: $('head meta[name="Description"]').attr('content'), link: url, item: items, diff --git a/lib/routes/3dmgame/news-center.ts b/lib/routes/3dmgame/news-center.ts index d51e0c760928..09ea2c102724 100644 --- a/lib/routes/3dmgame/news-center.ts +++ b/lib/routes/3dmgame/news-center.ts @@ -64,7 +64,7 @@ async function handler(ctx) { const out = await Promise.all(list.map((item) => parseArticle(item, cache.tryGet))); return { - title: '3DM - ' + $('title').text().split('_')[0], + title: '3DM - ' + $('title').text().split('_', 1)[0], description: $('meta[name="Description"]').attr('content'), link: url, item: out, diff --git a/lib/routes/3dmgame/utils.ts b/lib/routes/3dmgame/utils.ts index 37bda824243a..e714f989c81b 100644 --- a/lib/routes/3dmgame/utils.ts +++ b/lib/routes/3dmgame/utils.ts @@ -11,7 +11,7 @@ const parseArticle = (item, tryGet) => if (item.link.startsWith('https://dl.3dmgame.com/')) { const lis = $('.patchtop .lis'); - const [, category, pubDate, author] = lis.text().match(/补丁类型:(.*?)\n.*整理时间:(.*?)\n.*补丁制作:(.*?)\n/s); + const [, category, pubDate, author] = lis.text().match(/补丁类型:([^\n]*)\n.*整理时间:([^\n]*)\n.*补丁制作:([^\n]*)\n/s); item.description = lis.html() + $('.L_title').html() + $('.GmL_1').html(); item.category = category; diff --git a/lib/routes/3kns/index.tsx b/lib/routes/3kns/index.tsx index 7cab6030e233..eb3363bb8cfc 100644 --- a/lib/routes/3kns/index.tsx +++ b/lib/routes/3kns/index.tsx @@ -98,7 +98,11 @@ async function handler(ctx: Context): Promise { description: renderToString( baseUrl)} title={title} tid={$item.find('.jb-chakan').text().trim()} category={category} @@ -114,7 +118,7 @@ async function handler(ctx: Context): Promise { return { title: $('title').text(), - link: currentUrl.toString(), + link: currentUrl.href, allowEmpty: true, item: items, }; diff --git a/lib/routes/423down/index.ts b/lib/routes/423down/index.ts index c5fb36e757ac..84ad2ab49dcd 100644 --- a/lib/routes/423down/index.ts +++ b/lib/routes/423down/index.ts @@ -9,7 +9,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx) => { const { category = '' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 18; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 18; const domain = '423down.com'; const rootUrl = `https://www.${domain}`; @@ -83,7 +83,7 @@ export const handler = async (ctx) => { item.title = title; item.description = description; - item.pubDate = parseDate($$('p.meta-info').contents().first().text().trim().split(/\s/)[0], 'YYYY-MM-DD'); + item.pubDate = parseDate($$('p.meta-info').contents().first().text().trim().split(/\s/, 1)[0], 'YYYY-MM-DD'); item.category = $$('p.meta-info a[rel="category tag"]') .toArray() .map((c) => $$(c).text()); diff --git a/lib/routes/4chan/utils.tsx b/lib/routes/4chan/utils.tsx index 726523378e9e..f961ef8ecf16 100644 --- a/lib/routes/4chan/utils.tsx +++ b/lib/routes/4chan/utils.tsx @@ -46,7 +46,7 @@ const processCatalog = ({ data, board, viewOptions }: { data: CatalogApiReturn; description: renderToString(renderPost({ post: thread, board, viewOptions })), link: `https://boards.4chan.org/${board}/thread/${thread.no}`, pubDate: parseDate(thread.time * 1000), - title: thread.sub ?? sanitizeHtml(thread.com?.split('
')[0] ?? '', { allowedTags: [] }), + title: thread.sub ?? sanitizeHtml(thread.com?.split('
', 1)[0] ?? '', { allowedTags: [] }), })); }; diff --git a/lib/routes/4khd/category.ts b/lib/routes/4khd/category.ts index 34d93d5d856d..85e9d5f0e87c 100644 --- a/lib/routes/4khd/category.ts +++ b/lib/routes/4khd/category.ts @@ -37,9 +37,8 @@ async function handler(ctx) { const categoryUrl = `${SUB_URL}pages/${category}/`; const slug = category === 'album' ? 'photo' : category; - const { - data: [{ id: categoryId }], - } = await got(`${SUB_URL}wp-json/wp/v2/categories?slug=${slug}`); + const { data } = await got(`${SUB_URL}wp-json/wp/v2/categories?slug=${slug}`); + const categoryId = data[0].id; const { data: posts } = await got(`${SUB_URL}wp-json/wp/v2/posts?categories=${categoryId}&per_page=${limit}`); return { diff --git a/lib/routes/4ksj/forum.tsx b/lib/routes/4ksj/forum.tsx index 4762c3e87bb8..91c10204a7dd 100644 --- a/lib/routes/4ksj/forum.tsx +++ b/lib/routes/4ksj/forum.tsx @@ -114,7 +114,7 @@ async function handler(ctx) { const scriptUrl = new URL(scriptPath, rootUrl).href; const scriptResponse = await ofetch(scriptUrl); - const key = scriptResponse.match(/{var key="(.*?)"/)?.[1]; + const key = scriptResponse.match(/\{var key="(.*?)"/)?.[1]; const value = scriptResponse.match(/",value="(.*?)"/)?.[1]; const getPath = scriptResponse.match(/\.get\("(.*?&key=)"/)?.[1]; @@ -125,7 +125,7 @@ async function handler(ctx) { const cookieResponse = await ofetch.raw(`${rootUrl}${getPath}${key}&value=${md5(stringtoHex(value))}`); return cookieResponse.headers .getSetCookie() - .map((c) => c.split(';')[0]) + .map((c) => c.split(';', 1)[0]) .join('; '); }); diff --git a/lib/routes/4kup/category.ts b/lib/routes/4kup/category.ts index f4e2a64a23d7..fb34cf712e25 100644 --- a/lib/routes/4kup/category.ts +++ b/lib/routes/4kup/category.ts @@ -36,9 +36,8 @@ async function handler(ctx) { const category = ctx.req.param('category'); const categoryUrl = `${SUB_URL}category/${category}/`; - const { - data: [{ id: categoryId }], - } = await got(`${SUB_URL}wp-json/wp/v2/categories?slug=${category}`); + const { data } = await got(`${SUB_URL}wp-json/wp/v2/categories?slug=${category}`); + const categoryId = data[0].id; const { data: posts } = await got(`${SUB_URL}wp-json/wp/v2/posts?categories=${categoryId}&per_page=${limit}`); return { diff --git a/lib/routes/4kup/popular.ts b/lib/routes/4kup/popular.ts index 4f1e710180fd..f431c9034f9e 100644 --- a/lib/routes/4kup/popular.ts +++ b/lib/routes/4kup/popular.ts @@ -40,7 +40,8 @@ function getPeriodConfig(period) { range: 'last7days', title: `${SUB_NAME_PREFIX} - Top views in 7 days`, }; - } else if (period === '30') { + } + if (period === '30') { return { url: `${SUB_URL}hot-of-month/`, range: 'last30days', diff --git a/lib/routes/4kup/tag.ts b/lib/routes/4kup/tag.ts index 023dca2de270..e97841ca9054 100644 --- a/lib/routes/4kup/tag.ts +++ b/lib/routes/4kup/tag.ts @@ -36,9 +36,8 @@ async function handler(ctx) { const tag = ctx.req.param('tag'); const tagUrl = `${SUB_URL}tag/${tag}/`; - const { - data: [{ id: tagId }], - } = await got(`${SUB_URL}wp-json/wp/v2/tags?slug=${tag}`); + const { data } = await got(`${SUB_URL}wp-json/wp/v2/tags?slug=${tag}`); + const tagId = data[0].id; const { data: posts } = await got(`${SUB_URL}wp-json/wp/v2/posts?tags=${tagId}&per_page=${limit}`); return { diff --git a/lib/routes/51cto/recommend.ts b/lib/routes/51cto/recommend.ts index 1a5c21871351..27f78306c5b7 100644 --- a/lib/routes/51cto/recommend.ts +++ b/lib/routes/51cto/recommend.ts @@ -41,7 +41,7 @@ async function getFullcontent(item, cookie = '') { try { // More details: https://github.com/DIYgod/RSSHub/pull/16583#discussion_r1738643033 const _matches = articleResponse!.match(pattern)!.slice(0, 3); - const matches = _matches.map((str) => Number(str.split(':')[1])); + const matches = _matches.map((str) => Number(str.split(':', 2)[1])); const [v1, v2, v3] = matches; const cookie = '__tst_status=' + (v1 + v2 + v3) + '#;'; return await getFullcontent(item, cookie); @@ -65,7 +65,7 @@ async function handler(ctx) { const timestamp = Date.now(); const params = { page: 1, - page_size: ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 50, + page_size: ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 50, limit_time: 0, name_en: '', }; diff --git a/lib/routes/51cto/utils.ts b/lib/routes/51cto/utils.ts index 912612053b67..59566b87d8af 100644 --- a/lib/routes/51cto/utils.ts +++ b/lib/routes/51cto/utils.ts @@ -16,6 +16,6 @@ export const getToken = () => export const sign = (requestPath: string, payload: Record = {}, timestamp: number, token: string) => { payload.timestamp = timestamp; payload.token = token; - const sortedParams = Object.keys(payload).toSorted(); + const sortedParams = Object.keys(payload).toSorted((a, b) => a.localeCompare(b)); return md5(md5(requestPath) + md5(sortedParams + md5(token) + timestamp)); }; diff --git a/lib/routes/56kog/class.ts b/lib/routes/56kog/class.ts index 5c4394639680..19d60a0f5633 100644 --- a/lib/routes/56kog/class.ts +++ b/lib/routes/56kog/class.ts @@ -30,7 +30,7 @@ export const route: Route = { async function handler(ctx) { const { category = '1_1' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const currentUrl = new URL(`class/${category}.html`, rootUrl).href; diff --git a/lib/routes/56kog/top.ts b/lib/routes/56kog/top.ts index f9176fd65542..bca65ba704ec 100644 --- a/lib/routes/56kog/top.ts +++ b/lib/routes/56kog/top.ts @@ -26,9 +26,9 @@ export const route: Route = { async function handler(ctx) { const { category = 'weekvisit' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; - const currentUrl = new URL(`top/${category.split(/_/)[0]}_1.html`, rootUrl).href; + const currentUrl = new URL(`top/${category.split(/_/, 1)[0]}_1.html`, rootUrl).href; return await fetchItems(limit, currentUrl, cache.tryGet); } diff --git a/lib/routes/56kog/util.tsx b/lib/routes/56kog/util.tsx index 0748d86f1bcc..0673e02271ad 100644 --- a/lib/routes/56kog/util.tsx +++ b/lib/routes/56kog/util.tsx @@ -46,7 +46,7 @@ const fetchItems = async (limit, currentUrl, tryGet) => { const as = detail.find('a'); return { - label: detail.find('span.c-l-depths').text().split(/:/)[0], + label: detail.find('span.c-l-depths').text().split(/:/, 1)[0], value: as.length === 0 ? content( @@ -121,7 +121,7 @@ const renderDescription = ({ images, details }: { images?: Array<{ src?: string; {details.map((detail, index) => ( {detail.label} - {detail.value?.href && detail.value?.text ? {detail.value.text} : detail.value} + {detail.value?.href && detail.value.text ? {detail.value.text} : detail.value} ))} diff --git a/lib/routes/69shu/article.ts b/lib/routes/69shu/article.ts index bc13b6c30ce7..c38d92ded4a8 100644 --- a/lib/routes/69shu/article.ts +++ b/lib/routes/69shu/article.ts @@ -54,8 +54,8 @@ const createItem = (url: string) => cache.tryGet(url, async () => { const html = await get(url); const $ = load(html); - const { articleid, chapterid, chaptername } = parseObject(/bookinfo\s?=\s?{[\S\s]+?}/, $('head>script:not([src])').text()); - const decryptionMap = parseObject(/_\d+\s?=\s?{[\S\s]+?}/, $('.txtnav+script').text()); + const { articleid, chapterid, chaptername } = parseObject(/bookinfo\s?=\s?\{[\s\S]+?\}/, $('head>script:not([src])').text()); + const decryptionMap = parseObject(/_\d+\s?=\s?\{[\s\S]+?\}/, $('.txtnav+script').text()); return { title: chaptername, @@ -70,7 +70,7 @@ const parseObject = (reg: RegExp, str: string): Record => { const obj = {}; const match = reg.exec(str); if (match) { - for (const line of match[0].matchAll(/(\w+):\s?["']?([\S\s]+?)["']?[\n,}]/g)) { + for (const line of match[0].matchAll(/(\w+):\s?["']?([\s\S]+?)["']?[\n,}]/g)) { obj[line[1]] = line[2]; } } @@ -86,14 +86,17 @@ const decrypt = (txt: string, articleid: string, chapterid: string, decryptionMa const lineMap = {}; const articleKey = Number(articleid) + 3_061_711; const chapterKey = Number(chapterid) + 3_421_001; - for (const key of Object.keys(decryptionMap)) { - lineMap[(Number(key) ^ chapterKey) - articleKey] = (Number(decryptionMap[key]) ^ chapterKey) - articleKey; + for (const [key, value] of Object.entries(decryptionMap)) { + lineMap[(Number(key) ^ chapterKey) - articleKey] = (Number(value) ^ chapterKey) - articleKey; } return txt .replaceAll(/\u2003|\n/g, '') .split('

') - .flatMap((line, index, array) => (lineMap[index] ? array[lineMap[index]] : line).split('
')) + .flatMap((line, index, array) => { + const mapped = lineMap[index]; + return (mapped ? array[mapped] : line).split('
'); + }) .slice(1, -2) .join('

'); }; diff --git a/lib/routes/6park/index.ts b/lib/routes/6park/index.ts index 4e2015f42835..e3904f8f9342 100644 --- a/lib/routes/6park/index.ts +++ b/lib/routes/6park/index.ts @@ -66,7 +66,7 @@ async function handler(ctx) { const content = load(detailResponse.data); item.title = content('title').text().replace(' -6park.com', ''); - item.author = detailResponse.data.match(/送交者: .*>(.*)<.*\[/)[1]; + item.author = detailResponse.data.match(/送交者:[^>]*>([^<]*)<\/a>/)[1].trim(); item.pubDate = timezone(parseDate(detailResponse.data.match(/于 (.*) 已读/)[1], 'YYYY-MM-DD h:m'), +8); item.description = content('pre') .html() diff --git a/lib/routes/6park/news.ts b/lib/routes/6park/news.ts index 2050185e5d8e..30941bde8440 100644 --- a/lib/routes/6park/news.ts +++ b/lib/routes/6park/news.ts @@ -76,7 +76,7 @@ async function handler(ctx) { const content = load(detailResponse.data); - const matches = detailResponse.data.match(/新闻来源:(.*?)于.*(\d{4}(?:-\d{2}){2} (?:\d{1,2}:){2}\d{1,2})/); + const matches = detailResponse.data.match(/新闻来源:([^于]*)于.*(\d{4}(?:-\d{2}){2} (?:\d{1,2}:){2}\d{1,2})/); item.title = content('h2').text(); item.author = matches[1].trim(); diff --git a/lib/routes/6v123/index.ts b/lib/routes/6v123/index.ts index be6dfa62c9aa..8da5bd463402 100644 --- a/lib/routes/6v123/index.ts +++ b/lib/routes/6v123/index.ts @@ -12,7 +12,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const { category = 'dy' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '25', 10); + const limit = Number(ctx.req.query('limit') ?? '25'); const encoding = 'gb2312'; diff --git a/lib/routes/78dm/index.ts b/lib/routes/78dm/index.ts index 7a7be12bbae0..1fb95b2dea3c 100644 --- a/lib/routes/78dm/index.ts +++ b/lib/routes/78dm/index.ts @@ -10,7 +10,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx) => { const { category = 'news' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10; const rootUrl = 'https://www.78dm.net'; const currentUrl = new URL(category.includes('/') ? `${category}.html` : category, rootUrl).href; diff --git a/lib/routes/81/81rc/index.ts b/lib/routes/81/81rc/index.ts index e3687a4e0708..b9f6b974bbd8 100644 --- a/lib/routes/81/81rc/index.ts +++ b/lib/routes/81/81rc/index.ts @@ -8,7 +8,7 @@ import timezone from '@/utils/timezone'; export const handler = async (ctx) => { const { category = 'sy/gzdt_210283' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const rootUrl = 'https://81rc.81.cn'; const currentUrl = new URL(category?.endsWith('/') ? `${category}/` : category, rootUrl).href; diff --git a/lib/routes/8264/list.tsx b/lib/routes/8264/list.tsx index adcae9585e46..1ff23156917a 100644 --- a/lib/routes/8264/list.tsx +++ b/lib/routes/8264/list.tsx @@ -86,7 +86,7 @@ export const route: Route = { async function handler(ctx) { const { id = '751' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const rootUrl = 'https://www.8264.com'; const currentUrl = new URL(`list/${id}`, rootUrl).href; diff --git a/lib/routes/8kcos/cat.ts b/lib/routes/8kcos/cat.ts index f758aa1e5bbe..38195cb38da5 100644 --- a/lib/routes/8kcos/cat.ts +++ b/lib/routes/8kcos/cat.ts @@ -24,7 +24,7 @@ export const route: Route = { }; async function handler(ctx) { - const limit = Number.parseInt(ctx.req.query('limit') ?? 10, 10); + const limit = Number(ctx.req.query('limit') ?? 10); const { cat = '8kasianidol' } = ctx.req.param(); const categoryInfo = await getCategoryInfo(cat); const items = await getPosts(limit, { categories: categoryInfo.id }); diff --git a/lib/routes/8kcos/latest.ts b/lib/routes/8kcos/latest.ts index 3fb94126373c..5113cb041224 100644 --- a/lib/routes/8kcos/latest.ts +++ b/lib/routes/8kcos/latest.ts @@ -28,7 +28,7 @@ export const route: Route = { }; async function handler(ctx) { - const limit = Number.parseInt(ctx.req.query('limit') ?? 10, 10); + const limit = Number(ctx.req.query('limit') ?? 10); const items = await getPosts(limit); return { title: `${SUB_NAME_PREFIX}-最新`, diff --git a/lib/routes/8kcos/tag.ts b/lib/routes/8kcos/tag.ts index 3ffbad1fb878..928904912cb4 100644 --- a/lib/routes/8kcos/tag.ts +++ b/lib/routes/8kcos/tag.ts @@ -28,13 +28,13 @@ export const route: Route = { }; async function handler(ctx) { - const limit = Number.parseInt(ctx.req.query('limit') ?? 10, 10); + const limit = Number(ctx.req.query('limit') ?? 10); const tag = ctx.req.param('tag'); const tagInfo = await getTagInfo(tag); const items = await getPosts(limit, { tags: tagInfo.id }); return { - title: `${tagInfo.title}`, + title: tagInfo.title, link: `${SUB_URL}/tag/${tag}/`, item: items, }; diff --git a/lib/routes/95mm/utils.tsx b/lib/routes/95mm/utils.tsx index 49cb2562f344..be22b86a9c62 100644 --- a/lib/routes/95mm/utils.tsx +++ b/lib/routes/95mm/utils.tsx @@ -44,7 +44,7 @@ const ProcessItems = async (ctx, title, currentUrl) => { item.description = renderToString( <> {images.map((image) => ( - + ))} ); diff --git a/lib/routes/9to5/subsite.ts b/lib/routes/9to5/subsite.ts index 07b65cc641a7..2695c7559934 100644 --- a/lib/routes/9to5/subsite.ts +++ b/lib/routes/9to5/subsite.ts @@ -49,8 +49,10 @@ async function handler(ctx) { const feed = await parser.parseURL(link); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10; + const items = await Promise.all( - feed.items.splice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10).map((item) => + feed.items.splice(0, limit).map((item) => cache.tryGet(item.link, async () => { const response = await got({ method: 'get', diff --git a/lib/routes/9to5/utils.ts b/lib/routes/9to5/utils.ts index 3cbb329b1fc7..3a3542b1ea05 100644 --- a/lib/routes/9to5/utils.ts +++ b/lib/routes/9to5/utils.ts @@ -20,11 +20,12 @@ const ProcessFeed = (data) => { content.find('div.ad-disclaimer-container').remove(); content.find('div').each((i, e) => { - if ($(e)[0].attribs.class) { - const classes = $(e)[0].attribs.class; - if (/\w{10}\s\w{10}/g.test(classes)) { - $(e).remove(); - } + if (!$(e)[0].attribs.class) { + return; + } + const classes = $(e)[0].attribs.class; + if (/\w{10}\s\w{10}/.test(classes)) { + $(e).remove(); } }); diff --git a/lib/routes/a9vg/index.ts b/lib/routes/a9vg/index.ts index 2cbda0659faa..a38a324555ad 100644 --- a/lib/routes/a9vg/index.ts +++ b/lib/routes/a9vg/index.ts @@ -10,7 +10,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx) => { const { category = 'news/All' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const rootUrl = 'http://www.a9vg.com'; const currentUrl = new URL(`list/${category}`, rootUrl).href; diff --git a/lib/routes/aa1/60s.ts b/lib/routes/aa1/60s.ts index a6569c4b5906..b125d7e71a65 100644 --- a/lib/routes/aa1/60s.ts +++ b/lib/routes/aa1/60s.ts @@ -9,7 +9,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const { category } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '100', 10); + const limit = Number(ctx.req.query('limit') ?? '100'); const apiSlug = 'wp-json/wp/v2'; const baseUrl = 'https://60s.aa1.cn'; diff --git a/lib/routes/abc/index.ts b/lib/routes/abc/index.ts index a4eea519884e..7c1c969a9402 100644 --- a/lib/routes/abc/index.ts +++ b/lib/routes/abc/index.ts @@ -9,7 +9,7 @@ import { renderDescription } from './templates/description'; export const route: Route = { path: '/:category{.+}?', - example: '/wa', + example: '/abc/wa', radar: [ { source: ['abc.net.au/:category*'], @@ -34,7 +34,7 @@ The supported channels are all listed in the table below. For other channels, pl async function handler(ctx) { const { category = 'news/justin' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const rootUrl = 'https://www.abc.net.au'; const apiUrl = new URL('news-web/api/loader/channelrefetch', rootUrl).href; @@ -49,7 +49,7 @@ async function handler(ctx) { const feedUrl = new URL(`news/feed/${documentId}/rss.xml`, rootUrl).href; const feedResponse = await ofetch(feedUrl); - currentUrl = feedResponse.match(/([\w-./:?]+)<\/link>/)[1]; + currentUrl = feedResponse.match(/([\w./:?-]+)<\/link>/)[1]; } const currentResponse = await ofetch(currentUrl); @@ -73,7 +73,7 @@ async function handler(ctx) { description: renderDescription({ image: i.image ? { - src: i.image.imgSrc.split(/\?/)[0], + src: i.image.imgSrc.split(/\?/, 1)[0], alt: i.image.alt, } : undefined, @@ -86,7 +86,7 @@ async function handler(ctx) { if (i.mediaIndicator) { item.enclosure_type = 'audio/mpeg'; - item.itunes_item_image = i.image?.imgSrc.split(/\?/)[0] ?? undefined; + item.itunes_item_image = i.image?.imgSrc.split(/\?/, 1)[0] ?? undefined; item.itunes_duration = i.mediaIndicator.duration; } @@ -111,7 +111,7 @@ async function handler(ctx) { element.replaceWith( renderDescription({ image: { - src: element.find('img').prop('src').split(/\?/)[0], + src: element.find('img').prop('src').split(/\?/, 1)[0], alt: element.find('figcaption').text().trim(), }, }) @@ -124,14 +124,14 @@ async function handler(ctx) { item.title = content('meta[property="og:title"]').prop('content'); item.description = ''; - const enclosurePattern = String.raw`"(?:MIME|content)?Type":"([\w]+/[\w]+)".*?"(?:fileS|s)?ize":(\d+),.*?"url":"([\w-.:/?]+)"`; + const enclosurePattern = String.raw`"(?:MIME|content)?Type":"(\w+/\w+)".*?"(?:fileS|s)?ize":(\d+),.*?"url":"([\w.:/?-]+)"`; const enclosureMatches = detailResponse.match(new RegExp(enclosurePattern, 'g')); if (enclosureMatches) { const enclosureMatch = enclosureMatches .map((e) => e.match(new RegExp(enclosurePattern))) - .toSorted((a, b) => Number.parseInt(a[2], 10) - Number.parseInt(b[2], 10)) + .toSorted((a, b) => Number(a[2]) - Number(b[2])) .pop(); item.enclosure_url = enclosureMatch[3]; @@ -179,7 +179,7 @@ async function handler(ctx) { link: currentUrl, description: $('meta[property="og:description"]').prop('content'), language: $('html').prop('lang'), - image: $('meta[property="og:image"]').prop('content').split('?')[0], + image: $('meta[property="og:image"]').prop('content').split('?', 1)[0], icon, logo: icon, subtitle: $('meta[property="og:title"]').prop('content'), diff --git a/lib/routes/abc/templates/description.tsx b/lib/routes/abc/templates/description.tsx index 334ec2e93c36..bd407bfaa126 100644 --- a/lib/routes/abc/templates/description.tsx +++ b/lib/routes/abc/templates/description.tsx @@ -15,7 +15,7 @@ type DescriptionData = { }; const AbcDescription = ({ image, enclosure, description }: DescriptionData) => { - const enclosureTag = enclosure?.type?.split('/')[0] as keyof JSX.IntrinsicElements | undefined; + const enclosureTag = enclosure?.type?.split('/', 1)[0] as keyof JSX.IntrinsicElements | undefined; return ( <> diff --git a/lib/routes/abmedia/category.ts b/lib/routes/abmedia/category.ts index 4594821ce4b1..a99ea2b70f6b 100644 --- a/lib/routes/abmedia/category.ts +++ b/lib/routes/abmedia/category.ts @@ -6,7 +6,10 @@ const rootUrl = 'https://www.abmedia.io'; const cateAPIUrl = `${rootUrl}/wp-json/wp/v2/categories`; const postsAPIUrl = `${rootUrl}/wp-json/wp/v2/posts`; -const getCategoryId = (category) => got.get(`${cateAPIUrl}?slug=${category}`).then((res) => res.data[0].id); +const getCategoryId = async (category) => { + const res = await got.get(`${cateAPIUrl}?slug=${category}`); + return res.data[0].id; +}; export const route: Route = { path: '/:category?', diff --git a/lib/routes/accessbriefing/index.ts b/lib/routes/accessbriefing/index.ts index 9ea022076b30..05bd4a039f42 100644 --- a/lib/routes/accessbriefing/index.ts +++ b/lib/routes/accessbriefing/index.ts @@ -9,7 +9,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx) => { const { category = 'latest/news' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const rootUrl = 'https://www.accessbriefing.com'; const currentUrl = new URL(category, rootUrl).href; diff --git a/lib/routes/acfun/article.ts b/lib/routes/acfun/article.ts index fef9499e500a..ef3f183f6df2 100644 --- a/lib/routes/acfun/article.ts +++ b/lib/routes/acfun/article.ts @@ -45,7 +45,7 @@ export const route: Route = { parameters: { categoryId: { description: '分区 ID', - options: Object.keys(categoryMap).map((id) => ({ value: id, label: categoryMap[id].title })), + options: Object.entries(categoryMap).map(([id, value]) => ({ value: id, label: value.title })), }, sortType: { description: '排序', @@ -94,7 +94,7 @@ export const route: Route = { async function handler(ctx) { const { categoryId, sortType = 'createTime', timeRange = 'all' } = ctx.req.param(); - if (!categoryMap[categoryId]) { + if (!Object.hasOwn(categoryMap, categoryId)) { throw new InvalidParameterError(`Invalid category Id: ${categoryId}`); } if (!sortTypeEnum.has(sortType)) { diff --git a/lib/routes/acfun/bangumi.ts b/lib/routes/acfun/bangumi.ts index 01be818d10df..d29c818e959f 100644 --- a/lib/routes/acfun/bangumi.ts +++ b/lib/routes/acfun/bangumi.ts @@ -44,7 +44,7 @@ async function handler(ctx) { image: bangumiData.belongResource.coverImageV, item: bangumiList.items.map((item) => ({ title: `${item.episodeName}${item.title ? ` - ${item.title}` : ''}`, - description: renderDescription({ embed, aid: `ac${item.itemId}`, img: item.imgInfo.thumbnailImage.cdnUrls[0].url.split('?')[0] }), + description: renderDescription({ embed, aid: `ac${item.itemId}`, img: item.imgInfo.thumbnailImage.cdnUrls[0].url.split('?', 1)[0] }), link: `https://www.acfun.cn/bangumi/aa${id}_36188_${item.itemId}`, pubDate: parseDate(item.updateTime, 'x'), })), diff --git a/lib/routes/acfun/video.ts b/lib/routes/acfun/video.ts index 39ca0200be73..365e6ebc227e 100644 --- a/lib/routes/acfun/video.ts +++ b/lib/routes/acfun/video.ts @@ -40,7 +40,7 @@ async function handler(ctx) { const list = $('#ac-space-video-list a').toArray(); const image = $('head style:contains("user-photo")') .text() - .match(/.user-photo{\n\s*background:url\((.*)\) 0% 0% \/ 100% no-repeat;/)?.[1]; + .match(/.user-photo\{\n\s*background:url\((.*)\) 0% 0% \/ 100% no-repeat;/)?.[1]; return { title, @@ -59,7 +59,7 @@ async function handler(ctx) { return { title: itemTitle, - description: renderDescription({ embed, aid, img: itemImg?.split('?')[0] }), + description: renderDescription({ embed, aid, img: itemImg?.split('?', 1)[0] }), link: host + itemUrl, pubDate: parseDate(itemDate, 'YYYY/MM/DD'), }; diff --git a/lib/routes/acgvinyl/news.ts b/lib/routes/acgvinyl/news.ts index 3a8f618f0843..0b5e52622bc7 100644 --- a/lib/routes/acgvinyl/news.ts +++ b/lib/routes/acgvinyl/news.ts @@ -8,7 +8,7 @@ import { parseDate } from '@/utils/parse-date'; export const route: Route = { path: '/news', categories: ['anime'], - example: '/news', + example: '/acgvinyl/news', features: { requireConfig: false, requirePuppeteer: false, @@ -40,6 +40,7 @@ async function handler(ctx) { const newsIndexJsonText = $('script:contains("window.__INITIAL_STATE__")').text().replaceAll('window.__INITIAL_STATE__=', ''); const newsIndexJson = JSON.parse(newsIndexJsonText); + const limit = ctx.req.query('limit'); const newsListResponse = await ofetch(`${rootUrl}/rajax/news_h.jsp?cmd=getWafNotCk_getList`, { method: 'POST', headers: { @@ -47,7 +48,7 @@ async function handler(ctx) { }, body: new URLSearchParams({ page: '1', - pageSize: String(ctx.req.query('limit') ?? 20), + pageSize: String(limit ?? 20), fromMid: newsIndexJson.modules.module366.id, idList: `[${newsIndexJson.modules.module366.prop3}]`, sortKey: newsIndexJson.modules.module366.blob0.sortKey, diff --git a/lib/routes/acpaa/index.ts b/lib/routes/acpaa/index.ts index 60707c519458..b33d67603b1c 100644 --- a/lib/routes/acpaa/index.ts +++ b/lib/routes/acpaa/index.ts @@ -26,7 +26,7 @@ export const route: Route = { async function handler(ctx) { const { id = '1', name = '重要通知' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const rootUrl = 'http://www.acpaa.cn'; const currentUrl = new URL(`article/taglist.jhtml?tagIds=${id}&tagname=${name}`, rootUrl).href; diff --git a/lib/routes/acs/journal.tsx b/lib/routes/acs/journal.tsx index 4ad4d5587da1..d5f2b847817d 100644 --- a/lib/routes/acs/journal.tsx +++ b/lib/routes/acs/journal.tsx @@ -28,14 +28,14 @@ async function handler(ctx) { let title = ''; - const browser = await playwright(); + const context = await playwright(); const items = await cache.tryGet( currentUrl, async () => { - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' ? route.continue() : route.abort(); }); await page.goto(currentUrl, { waitUntil: 'domcontentloaded', @@ -76,7 +76,7 @@ async function handler(ctx) { false ); - await browser.close(); + await context.close(); return { title, diff --git a/lib/routes/adquan/case-library.ts b/lib/routes/adquan/case-library.ts index 49bc2804798f..5264e229da8a 100644 --- a/lib/routes/adquan/case-library.ts +++ b/lib/routes/adquan/case-library.ts @@ -13,7 +13,7 @@ import timezone from '@/utils/timezone'; import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '24', 10); + const limit = Number(ctx.req.query('limit') ?? '24'); const baseUrl = 'https://www.adquan.com'; const targetUrl: string = new URL('case_library/index', baseUrl).href; diff --git a/lib/routes/adquan/index.ts b/lib/routes/adquan/index.ts index 5a11ff58e430..aef56ed7e07c 100644 --- a/lib/routes/adquan/index.ts +++ b/lib/routes/adquan/index.ts @@ -13,7 +13,7 @@ import timezone from '@/utils/timezone'; import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://www.adquan.com'; const targetUrl: string = baseUrl; diff --git a/lib/routes/aeaweb/index.tsx b/lib/routes/aeaweb/index.tsx index 6d264824f660..d3ae3a53af01 100644 --- a/lib/routes/aeaweb/index.tsx +++ b/lib/routes/aeaweb/index.tsx @@ -68,7 +68,7 @@ async function handler(ctx) { item = $(item); return { - link: `${rootUrl}${item.attr('href').split('&')[0]}`, + link: `${rootUrl}${item.attr('href').split('&', 1)[0]}`, }; }); diff --git a/lib/routes/aeon/utils.tsx b/lib/routes/aeon/utils.tsx index 4ce6257dcf69..803d8fc35a92 100644 --- a/lib/routes/aeon/utils.tsx +++ b/lib/routes/aeon/utils.tsx @@ -109,7 +109,7 @@ export const getData = async (list) => { item.enclosure_type = 'audio/mpeg'; } else if (data.image?.url) { const imageUrl = data.image.url; - const cleanImageUrl = imageUrl.split('?')[0].toLowerCase(); + const cleanImageUrl = imageUrl.split('?', 1)[0].toLowerCase(); item.enclosure_url = imageUrl; if (cleanImageUrl.endsWith('.jpg') || cleanImageUrl.endsWith('.jpeg')) { diff --git a/lib/routes/aflcio/blog.ts b/lib/routes/aflcio/blog.ts index 55fe58542489..d05d11a013de 100644 --- a/lib/routes/aflcio/blog.ts +++ b/lib/routes/aflcio/blog.ts @@ -10,7 +10,7 @@ import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '5', 10); + const limit = Number(ctx.req.query('limit') ?? '5'); const baseUrl = 'https://aflcio.org'; const targetUrl: string = new URL('blog', baseUrl).href; diff --git a/lib/routes/agefans/update.ts b/lib/routes/agefans/update.ts index c7dbf3dd33bc..6121640147ec 100644 --- a/lib/routes/agefans/update.ts +++ b/lib/routes/agefans/update.ts @@ -57,10 +57,12 @@ async function handler() { const content = load(detailResponse.data); content('img').each((_, ele) => { - if (ele.attribs['data-original']) { - ele.attribs.src = ele.attribs['data-original']; - delete ele.attribs['data-original']; + if (!ele.attribs['data-original']) { + return; } + + ele.attribs.src = ele.attribs['data-original']; + delete ele.attribs['data-original']; }); content('.video_detail_collect').remove(); diff --git a/lib/routes/agora0/pen0.ts b/lib/routes/agora0/pen0.ts index 664e4aaeb2c1..8f65d5ee8a89 100644 --- a/lib/routes/agora0/pen0.ts +++ b/lib/routes/agora0/pen0.ts @@ -43,8 +43,8 @@ async function handler() { return { title: item.find('h3').text(), link: item.find('h3 a').attr('href'), - author: meta.split('|')[0].trim(), - pubDate: parseDate(meta.split('|')[1].trim()), + author: meta.split('|', 1)[0].trim(), + pubDate: parseDate(meta.split('|', 2)[1].trim()), }; }); diff --git a/lib/routes/agri/index.ts b/lib/routes/agri/index.ts index 2e5697cb0bd5..308c0cb87786 100644 --- a/lib/routes/agri/index.ts +++ b/lib/routes/agri/index.ts @@ -10,7 +10,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx) => { const { category = 'zx/zxfb/' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10; const rootUrl = 'http://www.agri.cn'; const currentUrl = new URL(category.endsWith('/') ? category : `${category}/`, rootUrl).href; diff --git a/lib/routes/ai-bot/daily-ai-news.ts b/lib/routes/ai-bot/daily-ai-news.ts index d2de7b0d21ae..13f42a0cf2ba 100755 --- a/lib/routes/ai-bot/daily-ai-news.ts +++ b/lib/routes/ai-bot/daily-ai-news.ts @@ -21,8 +21,8 @@ function parseDateString(dateStr: string, ctx: DateContext): Date | undefined { return undefined; } - const month = Number.parseInt(match[1], 10); - const day = Number.parseInt(match[2], 10); + const month = Number(match[1]); + const day = Number(match[2]); // 检测跨年:如果当前日期比上一个日期大,说明跨年了 if (ctx.prevMonth > 0 && (month > ctx.prevMonth || (month === ctx.prevMonth && day > ctx.prevDay))) { diff --git a/lib/routes/aibase/daily.ts b/lib/routes/aibase/daily.ts index 9d83c5b60195..e3c7efcdf1bf 100644 --- a/lib/routes/aibase/daily.ts +++ b/lib/routes/aibase/daily.ts @@ -14,7 +14,7 @@ export const route: Route = { maintainers: ['3tuuu'], handler: async (ctx) => { // 每页数量限制 - const limit = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); // 用项目中已有的获取页面方法,获取页面以及 Token const currentUrl = new URL('discover', rootUrl).href; const currentHtml = await ofetch(currentUrl); diff --git a/lib/routes/aibase/discover.ts b/lib/routes/aibase/discover.ts index 14e27b256fab..fa413dbcc11d 100644 --- a/lib/routes/aibase/discover.ts +++ b/lib/routes/aibase/discover.ts @@ -10,7 +10,7 @@ export const handler = async (ctx) => { const [pid, sid] = id?.split(/-/) ?? [undefined, undefined]; - const limit = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const currentUrl = new URL(`discover${id ? `/${id}` : ''}`, rootUrl).href; diff --git a/lib/routes/aibase/news.ts b/lib/routes/aibase/news.ts index 83cd1ebf0b1a..60e8cf444664 100644 --- a/lib/routes/aibase/news.ts +++ b/lib/routes/aibase/news.ts @@ -13,7 +13,7 @@ export const route: Route = { maintainers: ['zreo0'], handler: async (ctx) => { // 每页数量限制 - const limit = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); // 用项目中已有的获取页面方法,获取页面以及 Token const currentUrl = new URL('discover', rootUrl).href; const currentHtml = await ofetch(currentUrl); diff --git a/lib/routes/aibase/topic.ts b/lib/routes/aibase/topic.ts index e0ddf9ac9399..c7e6c623f1c6 100644 --- a/lib/routes/aibase/topic.ts +++ b/lib/routes/aibase/topic.ts @@ -8,7 +8,7 @@ import { buildApiUrl, processItems, rootUrl } from './util'; export const handler = async (ctx) => { const { id, filter = 'id' } = ctx.req.param(); - const limit = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const currentUrl = new URL(id ? `topic/${id}` : 'discover', rootUrl).href; diff --git a/lib/routes/ainvest/article.ts b/lib/routes/ainvest/article.ts index 77f827f4b0d0..a638d3a357fb 100644 --- a/lib/routes/ainvest/article.ts +++ b/lib/routes/ainvest/article.ts @@ -26,7 +26,7 @@ export const route: Route = { }; async function handler(ctx) { - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 5; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 5; const items = await fetchContentItems([109], limit); return { diff --git a/lib/routes/ainvest/news.ts b/lib/routes/ainvest/news.ts index cf45cb879db5..8aeb08e17822 100644 --- a/lib/routes/ainvest/news.ts +++ b/lib/routes/ainvest/news.ts @@ -28,7 +28,7 @@ export const route: Route = { }; async function handler(ctx) { - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 5; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 5; const streamIds = [109, 416, 438, 529, 721, 834, 835]; const items = await fetchContentItems(streamIds, limit); diff --git a/lib/routes/aip/journal-pupp.ts b/lib/routes/aip/journal-pupp.ts index 845661a89223..3e0412b5a127 100644 --- a/lib/routes/aip/journal-pupp.ts +++ b/lib/routes/aip/journal-pupp.ts @@ -18,12 +18,12 @@ const handler = async (ctx) => { } // use Playwright due to the obstacle by cloudflare challenge - const browser = await playwright(); + const context = await playwright(); const { jrnlName, list } = await cache.tryGet( jrnlUrl, async () => { - const response = await playwrightGet(jrnlUrl, browser); + const response = await playwrightGet(jrnlUrl, context); const $ = load(response); const jrnlName = $('.header-journal-title').text(); const list = $('.card') @@ -52,7 +52,7 @@ const handler = async (ctx) => { false ); - await browser.close(); + await context.close(); return { title: jrnlName, diff --git a/lib/routes/aip/journal.ts b/lib/routes/aip/journal.ts index 68bea00e2867..f5c718e57e89 100644 --- a/lib/routes/aip/journal.ts +++ b/lib/routes/aip/journal.ts @@ -43,7 +43,7 @@ async function handler(ctx) { const $ = load(response); const jrnlName = $('meta[property="og:title"]') .attr('content') - .match(/(?:[^=]*=)?\s*([^>]+)\s*/)[1]; + .match(/(?:[^=]*=)?\s*([^>]+)/)[1]; const publication = $('.al-article-item-wrap.al-normal'); const list = publication.toArray().map((item) => { diff --git a/lib/routes/aip/utils.tsx b/lib/routes/aip/utils.tsx index ba592557f627..00826b987be3 100644 --- a/lib/routes/aip/utils.tsx +++ b/lib/routes/aip/utils.tsx @@ -1,11 +1,11 @@ import { renderToString } from 'hono/jsx/dom/server'; -const playwrightGet = async (url, browser) => { - const page = await browser.newPage(); +const playwrightGet = async (url, context) => { + const page = await context.newPage(); // await page.setExtraHTTPHeaders({ referer: host }); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' ? request.continue() : request.abort(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' ? route.continue() : route.abort(); }); await page.goto(url, { waitUntil: 'domcontentloaded', diff --git a/lib/routes/air-level/levelrank.ts b/lib/routes/air-level/levelrank.ts index 856466be7ba4..de24aca9bb5a 100644 --- a/lib/routes/air-level/levelrank.ts +++ b/lib/routes/air-level/levelrank.ts @@ -4,7 +4,7 @@ import type { Route } from '@/types'; import ofetch from '@/utils/ofetch'; // 统一使用的请求库 export const route: Route = { - path: ['/rank/:status?'], + path: '/rank/:status?', radar: [ { source: ['m.air-level.com/rank/:status', 'm.air-level.com/rank'], diff --git a/lib/routes/aisixiang/column.ts b/lib/routes/aisixiang/column.ts index ba0d9f5363dd..537bec3e9b1e 100644 --- a/lib/routes/aisixiang/column.ts +++ b/lib/routes/aisixiang/column.ts @@ -28,7 +28,7 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const currentUrl = new URL(`/data/search?column=${id}`, rootUrl).href; @@ -49,7 +49,7 @@ async function handler(ctx) { return { title: a.text(), link: new URL(a.prop('href'), rootUrl).href, - author: a.text().split(':')[0], + author: a.text().split(':', 1)[0], pubDate: timezone(parseDate(item.find('span').text()), +8), }; }); diff --git a/lib/routes/aisixiang/thinktank.ts b/lib/routes/aisixiang/thinktank.ts index d32d70799685..5bf2966f2155 100644 --- a/lib/routes/aisixiang/thinktank.ts +++ b/lib/routes/aisixiang/thinktank.ts @@ -29,7 +29,7 @@ export const route: Route = { async function handler(ctx) { const { id, type = '' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const currentUrl = new URL(`thinktank/${id}.html`, rootUrl).href; diff --git a/lib/routes/aisixiang/toplist.ts b/lib/routes/aisixiang/toplist.ts index 3599fe035e94..510f90f37a71 100644 --- a/lib/routes/aisixiang/toplist.ts +++ b/lib/routes/aisixiang/toplist.ts @@ -19,7 +19,7 @@ export const route: Route = { async function handler(ctx) { const { id = '1', period = '1' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const currentUrl = new URL(`toplist${id ? `?id=${id}${id === '1' ? `&period=${period}` : ''}` : ''}`, rootUrl).href; @@ -27,7 +27,7 @@ async function handler(ctx) { const $ = load(response); - const title = `${$('a.hl').text() || ''}${$('title').text().split('_')[0]}`; + const title = `${$('a.hl').text() || ''}${$('title').text().split('_', 1)[0]}`; const items = $('div.tops_list') .slice(0, limit) diff --git a/lib/routes/aisixiang/utils.ts b/lib/routes/aisixiang/utils.ts index 94ba3e62ecfd..84fc1e56b0e1 100644 --- a/lib/routes/aisixiang/utils.ts +++ b/lib/routes/aisixiang/utils.ts @@ -29,8 +29,8 @@ const ProcessFeed = (limit, tryGet, items) => .toArray() .map((c) => content(c).text()); item.pubDate = timezone(parseDate(content('div.info').text().split('时间:').pop()), +8); - item.upvotes = content('span.like-num').text() ? Number.parseInt(content('span.like-num').text(), 10) : 0; - item.comments = commentMatches ? Number.parseInt(commentMatches[1], 10) : 0; + item.upvotes = content('span.like-num').text() ? Number(content('span.like-num').text()) : 0; + item.comments = commentMatches ? Number(commentMatches[1]) : 0; return item; }) diff --git a/lib/routes/aisixiang/zhuanti.ts b/lib/routes/aisixiang/zhuanti.ts index d6a9741212bf..de3175a73f7b 100644 --- a/lib/routes/aisixiang/zhuanti.ts +++ b/lib/routes/aisixiang/zhuanti.ts @@ -31,7 +31,7 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const currentUrl = new URL(`zhuanti/${id}.html`, rootUrl).href; @@ -52,7 +52,7 @@ async function handler(ctx) { return { title: a.text(), link: new URL(a.prop('href'), rootUrl).href, - author: a.text().split(':')[0], + author: a.text().split(':', 1)[0], pubDate: timezone(parseDate(item.find('span').text()), +8), }; }); diff --git a/lib/routes/ali213/news.ts b/lib/routes/ali213/news.ts index f19844e862d4..1fd9538f7af5 100644 --- a/lib/routes/ali213/news.ts +++ b/lib/routes/ali213/news.ts @@ -14,7 +14,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { category = 'new' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const rootUrl = 'https://www.ali213.net'; const targetUrl: string = new URL(`news/${category.endsWith('/') ? category : `${category}/`}`, rootUrl).href; @@ -134,7 +134,7 @@ export const handler = async (ctx: Context): Promise => { ...item, title, description, - pubDate: timezone(parseDate($$('div.newstag_l').text().split(/\s/)[0]), +8), + pubDate: timezone(parseDate($$('div.newstag_l').text().split(/\s/, 1)[0]), +8), content: { html: description, text: $$('div#Content').html() ?? '', diff --git a/lib/routes/ali213/zl.ts b/lib/routes/ali213/zl.ts index 13c7cdf1a64f..c93d750f982e 100644 --- a/lib/routes/ali213/zl.ts +++ b/lib/routes/ali213/zl.ts @@ -13,7 +13,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { category } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '1', 10); + const limit = Number(ctx.req.query('limit') ?? '1'); const rootUrl = 'https://www.ali213.net'; const apiRootUrl = 'https://mp.ali213.net'; diff --git a/lib/routes/alicesoft/infomation.ts b/lib/routes/alicesoft/infomation.ts index 0cffbbdf455f..305013091b97 100644 --- a/lib/routes/alicesoft/infomation.ts +++ b/lib/routes/alicesoft/infomation.ts @@ -36,7 +36,7 @@ export const route: Route = { async function handler(ctx) { const { category, game } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10; let url = `${baseUrl}/information`; if (category) { diff --git a/lib/routes/aljazeera/index.tsx b/lib/routes/aljazeera/index.tsx index fb1f5a7cbf4b..1e64c901373e 100644 --- a/lib/routes/aljazeera/index.tsx +++ b/lib/routes/aljazeera/index.tsx @@ -71,8 +71,9 @@ async function handler(ctx) { }; }); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 50; items = await Promise.all( - items.slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 50).map((item) => + items.slice(0, limit).map((item) => cache.tryGet(item.link, async () => { const detailResponse = await ofetch(item.link); diff --git a/lib/routes/ally/rail.ts b/lib/routes/ally/rail.ts index c7c67e5b886c..8f5d6dbc5144 100644 --- a/lib/routes/ally/rail.ts +++ b/lib/routes/ally/rail.ts @@ -83,7 +83,7 @@ async function handler(ctx) { .filter(Boolean); const uniqueItems: DataItem[] = []; for (const item of items) { - if (!uniqueItems.some((uniqueItem) => uniqueItem.link === item?.link)) { + if (uniqueItems.every((uniqueItem) => uniqueItem.link !== item?.link)) { uniqueItems.push(item!); } } diff --git a/lib/routes/alternativeto/utils.ts b/lib/routes/alternativeto/utils.ts index 08fd66814fef..8160f19c5471 100644 --- a/lib/routes/alternativeto/utils.ts +++ b/lib/routes/alternativeto/utils.ts @@ -4,17 +4,17 @@ const baseURL = 'https://alternativeto.net'; const playwrightGet = (url, cache) => cache.tryGet(url, async () => { - const browser = await playwright(); - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' ? request.continue() : request.abort(); + const context = await playwright(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' ? route.continue() : route.abort(); }); await page.goto(url, { waitUntil: 'domcontentloaded', }); const html = await page.evaluate(() => document.documentElement.innerHTML); - await browser.close(); + await context.close(); return html; }); diff --git a/lib/routes/altotrain/news.ts b/lib/routes/altotrain/news.ts index f541b8ed2bb3..8bfbe028df29 100644 --- a/lib/routes/altotrain/news.ts +++ b/lib/routes/altotrain/news.ts @@ -71,7 +71,7 @@ function extractItem(a: Cheerio, language: string) { const descEl = a.find('p').first(); const description = descEl.text().trim(); - const dateMatch = language === 'fr' ? description.match(/(\d{1,2} [a-zéû]+[.]? \d{4})/i) : description.match(/([A-Z][a-z]+[.]? \d{1,2}, \d{4})/); + const dateMatch = language === 'fr' ? description.match(/(\d{1,2} [a-zéû]+\.? \d{4})/i) : description.match(/([A-Z][a-z]+\.? \d{1,2}, \d{4})/); const pubDateStr = dateMatch ? dateMatch[1].trim() : ''; const pubDate = parseDate(pubDateStr); diff --git a/lib/routes/amazfitwatchfaces/index.ts b/lib/routes/amazfitwatchfaces/index.ts index 1806b8dc0866..a4a54df79695 100644 --- a/lib/routes/amazfitwatchfaces/index.ts +++ b/lib/routes/amazfitwatchfaces/index.ts @@ -13,7 +13,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { device, sort, searchParams } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://amazfitwatchfaces.com'; const targetUrl: string = new URL(`${device}/${sort}${searchParams ? `?${searchParams}` : ''}`, baseUrl).href; diff --git a/lib/routes/annualreviews/index.ts b/lib/routes/annualreviews/index.ts index 1abce26b925f..b4a223bf6bd7 100644 --- a/lib/routes/annualreviews/index.ts +++ b/lib/routes/annualreviews/index.ts @@ -59,7 +59,7 @@ async function handler(ctx) { doi, guid: doi, title: item.find('title').text(), - link: item.find('link').attr('href').split('?')[0], + link: item.find('link').attr('href').split('?', 1)[0], description: item.find('content').text(), pubDate: parseDate(item.find('published').text()), author: item diff --git a/lib/routes/anthropic/engineering.ts b/lib/routes/anthropic/engineering.ts index a07974b14895..acb0435dfd5d 100644 --- a/lib/routes/anthropic/engineering.ts +++ b/lib/routes/anthropic/engineering.ts @@ -26,7 +26,7 @@ async function handler(ctx) { const link = `${baseUrl}/engineering`; const response = await ofetch(link); const $ = load(response); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const list: DataItem[] = $('a[class*="cardLink"]') .toArray() diff --git a/lib/routes/anthropic/news.ts b/lib/routes/anthropic/news.ts index 9708029ef637..52c05e8fc836 100644 --- a/lib/routes/anthropic/news.ts +++ b/lib/routes/anthropic/news.ts @@ -25,7 +25,7 @@ async function handler(ctx) { const link = 'https://www.anthropic.com/news'; const response = await ofetch(link); const $ = load(response); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10; const list: DataItem[] = $('[class^="PublicationList-module-scss-module__"][class$="__list"] a') .toArray() diff --git a/lib/routes/anthropic/research.ts b/lib/routes/anthropic/research.ts index cfba01932e4d..9a7baa33b66d 100644 --- a/lib/routes/anthropic/research.ts +++ b/lib/routes/anthropic/research.ts @@ -35,9 +35,8 @@ async function handler() { const text = $e.text(); const match = regexp.exec(text); if (match) { - let data; try { - data = JSON.parse(match[1]); + const data = JSON.parse(match[1]); if (Array.isArray(data) && data.length === 2 && data[0] === 1) { textList.push(data[1]); } @@ -47,7 +46,7 @@ async function handler() { } } - const partRegex = /^([0-9a-zA-Z]+):([0-9a-zA-Z]+)?(\[.*)$/; + const partRegex = /^([0-9a-z]+):([0-9a-z]+)?(\[.*)$/i; const fd = textList .join('') .split('\n') diff --git a/lib/routes/anytxt/release-notes.ts b/lib/routes/anytxt/release-notes.ts index 32901623dfa9..6b4535f86359 100644 --- a/lib/routes/anytxt/release-notes.ts +++ b/lib/routes/anytxt/release-notes.ts @@ -9,7 +9,7 @@ import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://anytxt.net'; const targetUrl: string = new URL('download/', baseUrl).href; @@ -28,7 +28,7 @@ export const handler = async (ctx: Context): Promise => { const title: string = $el.text(); const description: string | undefined = $el.next().html() ?? ''; - const pubDateStr: string | undefined = title.split(/\s/)[0]; + const pubDateStr: string | undefined = title.split(/\s/, 1)[0]; const linkUrl: string | undefined = targetUrl; const upDatedStr: string | undefined = pubDateStr; diff --git a/lib/routes/apkpure/versions.ts b/lib/routes/apkpure/versions.ts index 911f05d2ae65..bce6bb781d1e 100644 --- a/lib/routes/apkpure/versions.ts +++ b/lib/routes/apkpure/versions.ts @@ -28,11 +28,11 @@ async function handler(ctx) { const baseUrl = 'https://apkpure.com'; const link = `${baseUrl}/${region}/${pkg}/versions`; - const browser = await playwright(); - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' ? request.continue() : request.abort(); + const context = await playwright(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' ? route.continue() : route.abort(); }); logger.http(`Requesting ${link}`); await page.goto(link, { @@ -40,7 +40,7 @@ async function handler(ctx) { }); const r = await page.evaluate(() => document.documentElement.innerHTML); - await browser.close(); + await context.close(); const $ = load(r); const img = new URL($('.ver-top img').attr('src')); diff --git a/lib/routes/apnews/mobile-api.ts b/lib/routes/apnews/mobile-api.ts index 3c894ca3a36e..7263daa9dc03 100644 --- a/lib/routes/apnews/mobile-api.ts +++ b/lib/routes/apnews/mobile-api.ts @@ -10,12 +10,12 @@ import { fetchArticle } from './utils'; export const route: Route = { path: '/mobile/:path{.+}?', categories: ['traditional-media'], - example: '/apnews/mobile/ap-top-news', + example: '/apnews/mobile', view: ViewType.Articles, parameters: { path: { description: 'Corresponding path from AP News website', - default: 'ap-top-news', + default: '/', }, }, features: { @@ -37,7 +37,7 @@ export const route: Route = { }; async function handler(ctx) { - const path = ctx.req.param('path') ? `/${ctx.req.param('path')}` : '/hub/ap-top-news'; + const path = ctx.req.param('path') ? `/${ctx.req.param('path')}` : '/'; const apiRootUrl = 'https://apnews.com/graphql/delivery/ap/v1'; const res = await ofetch(apiRootUrl, { query: { @@ -72,20 +72,20 @@ async function handler(ctx) { description: e.description, guid: e.id, }; - } else if (e.__typename === 'VideoPlaylistItem') { + } + if (e.__typename === 'VideoPlaylistItem') { return { title: e.title, link: e.url, description: e.description, guid: e.contentId, }; - } else { - return; } + return; }) .filter(Boolean) .toSorted((a, b) => b.pubDate - a.pubDate) - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20); + .slice(0, ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20); const items = ctx.req.query('fulltext') === 'true' ? await pMap(list, (item) => fetchArticle(item), { concurrency: 10 }) : list; diff --git a/lib/routes/apnews/sitemap.ts b/lib/routes/apnews/sitemap.ts index 48f68db257a2..3d61a0c0afcc 100644 --- a/lib/routes/apnews/sitemap.ts +++ b/lib/routes/apnews/sitemap.ts @@ -84,7 +84,7 @@ async function handler(ctx) { }) .filter((e) => Boolean(e.link) && !new URL(e.link).pathname.split('/').includes('hub')) .toSorted((a, b) => (a.pubDate && b.pubDate ? b.pubDate - a.pubDate : b.lastmod - a.lastmod)) - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20); + .slice(0, ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20); const items = ctx.req.query('fulltext') === 'true' ? await pMap(list, (item) => fetchArticle(item), { concurrency: 20 }) : list; diff --git a/lib/routes/apnews/topics.ts b/lib/routes/apnews/topics.ts index 7c955205ce51..cdb8eeb9194a 100644 --- a/lib/routes/apnews/topics.ts +++ b/lib/routes/apnews/topics.ts @@ -10,7 +10,7 @@ import { fetchArticle, removeDuplicateByKey } from './utils'; const HOME_PAGE = 'https://apnews.com'; export const route: Route = { - path: ['/topics/:topic?', '/nav/:nav{.*}?'], + path: ['/topics/:topic?', '/nav/:nav{.+}?'], categories: ['traditional-media'], example: '/apnews/topics/apf-topnews', view: ViewType.Articles, @@ -41,7 +41,7 @@ export const route: Route = { async function handler(ctx) { const { topic = 'trending-news', nav = '' } = ctx.req.param(); - const useNav = ctx.req.routePath === '/apnews/nav/:nav{.*}?'; + const useNav = ctx.req.routePath === '/apnews/nav/:nav{.+}?'; const url = useNav ? `${HOME_PAGE}/${nav}` : `${HOME_PAGE}/hub/${topic}`; const response = await got(url); const $ = load(response.data); diff --git a/lib/routes/apnews/utils.ts b/lib/routes/apnews/utils.ts index 083c19fb9aa7..6225b23dec50 100644 --- a/lib/routes/apnews/utils.ts +++ b/lib/routes/apnews/utils.ts @@ -5,7 +5,7 @@ import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; export function removeDuplicateByKey(items, key: string) { - return [...new Map(items.map((x) => [x[key], x])).values()]; + return new Map(items.map((x) => [x[key], x])).values().toArray(); } export function fetchArticle(item) { @@ -25,9 +25,8 @@ export function fetchArticle(item) { author: gtmParsed.author, ...item, }; - } else { - return item; } + return item; } const rawLdjson = JSON.parse($('#link-ld-json').text()); let ldjson; @@ -45,23 +44,22 @@ export function fetchArticle(item) { description: $('div.RichTextStoryBody').html() || $(':is(.VideoLead, .VideoPage-pageSubHeading)').html(), category: [...(section ? [section] : []), ...(ldjson.keywords ?? [])], guid: $("meta[name='brightspot.contentId']").attr('content'), - author: ldjson.author?.map((e) => e.mainEntity), + author: ldjson.author, }; - } else { - // Live - ldjson = rawLdjson; + } + // Live + ldjson = rawLdjson; - const url = new URL(item.link); - const description = url.hash ? $(url.hash).parent().find('.LiveBlogPost-body').html() : ldjson.description; - const pubDate = url.hash ? parseDate(Number.parseInt($(url.hash).parent().attr('data-posted-date-timestamp'), 10)) : parseDate(ldjson.coverageStartTime); + const url = new URL(item.link); + const description = url.hash ? $(url.hash).parent().find('.LiveBlogPost-body').html() : ldjson.description; + const pubDate = url.hash ? parseDate(Number($(url.hash).parent().attr('data-posted-date-timestamp'))) : parseDate(ldjson.coverageStartTime); - return { - ...item, - category: ldjson.keywords, - pubDate, - description, - guid: $("meta[name='brightspot.contentId']").attr('content'), - }; - } + return { + ...item, + category: ldjson.keywords, + pubDate, + description, + guid: $("meta[name='brightspot.contentId']").attr('content'), + }; }); } diff --git a/lib/routes/app-center/release.tsx b/lib/routes/app-center/release.tsx index 65a235222a87..a41da5c3f164 100644 --- a/lib/routes/app-center/release.tsx +++ b/lib/routes/app-center/release.tsx @@ -158,7 +158,7 @@ async function handler(ctx) { const releaseResponse = await got(item.link); const releaseInfo = releaseResponse.data; - const userName = releaseInfo.owner.display_name; + const username = releaseInfo.owner.display_name; const appOS = releaseInfo.app_os; const shortVersion = releaseInfo.short_version; // will be an empty string for Windows const versionCode = releaseInfo.version; @@ -181,7 +181,7 @@ async function handler(ctx) { const appName = releaseInfo.app_display_name; const distributionGroupId = releaseInfo.distribution_group_id; const distributionGroupName = releaseInfo.distribution_groups.find((group) => group.id === distributionGroupId).display_name; - item._feed_title = `${appName} (${distributionGroupName}) for ${appOS} by ${userName} - App Center Releases`; + item._feed_title = `${appName} (${distributionGroupName}) for ${appOS} by ${username} - App Center Releases`; item._feed_icon = releaseInfo.app_icon_url; const version = shortVersion && versionCode ? `${shortVersion} (${versionCode})` : shortVersion || versionCode; @@ -193,7 +193,7 @@ async function handler(ctx) { (isExternalBuild ? '[External Build]' : '') + `Version ${version}`; item.link = link; // replace the link with the release page - item.author = userName; + item.author = username; item.description = renderDescription({ releaseDate, sizeInMBytes, diff --git a/lib/routes/app-sales/index.ts b/lib/routes/app-sales/index.ts index f0c772d3680d..ffbc99d8f2d6 100644 --- a/lib/routes/app-sales/index.ts +++ b/lib/routes/app-sales/index.ts @@ -10,7 +10,7 @@ import { baseUrl, fetchItems } from './util'; export const handler = async (ctx: Context): Promise => { const { category = 'highlights', country = 'us' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '100', 10); + const limit = Number(ctx.req.query('limit') ?? '100'); const targetUrl: string = new URL(category.endsWith('/') ? category : `${category}/`, baseUrl).href; diff --git a/lib/routes/app-sales/mostwanted.ts b/lib/routes/app-sales/mostwanted.ts index 49995398da6f..90ea98339222 100644 --- a/lib/routes/app-sales/mostwanted.ts +++ b/lib/routes/app-sales/mostwanted.ts @@ -10,7 +10,7 @@ import { baseUrl, fetchItems } from './util'; export const handler = async (ctx: Context): Promise => { const { time = '24h', country = 'us' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const targetUrl: string = new URL('mostwanted/', baseUrl).href; diff --git a/lib/routes/apple/apps.ts b/lib/routes/apple/apps.ts index 402e4e8c6967..cf854838fb5c 100644 --- a/lib/routes/apple/apps.ts +++ b/lib/routes/apple/apps.ts @@ -79,7 +79,7 @@ async function handler(ctx) { } platform = undefined; - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 100; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 100; const rootUrl = 'https://apps.apple.com'; const currentUrl = new URL(`${country}/app/${id}`, rootUrl).href; @@ -129,8 +129,7 @@ async function handler(ctx) { image = platformAttribute.iconArtwork?.url?.replace('{w}x{h}{c}.{f}', '3000x3000bb.webp'); } else { title = appName; - for (const pid of Object.keys(platformAttributes)) { - const platformAttribute = platformAttributes[pid]; + for (const [pid, platformAttribute] of Object.entries(platformAttributes)) { items = [ ...items, ...platformAttribute.versionHistory.map((v) => ({ diff --git a/lib/routes/apple/newsroom.ts b/lib/routes/apple/newsroom.ts index ea915df082c4..949b2dce19c0 100644 --- a/lib/routes/apple/newsroom.ts +++ b/lib/routes/apple/newsroom.ts @@ -54,7 +54,7 @@ const fetchArticle = (item: Item & { link: string }) => }); async function handler(ctx) { - const limit = Math.max(Number.parseInt(ctx.req.query('limit') ?? '', 10) || defaultLimit, 1); + const limit = Number(ctx.req.query('limit')) || defaultLimit; const feedResponse = await ofetch(feedUrl, { parseResponse: (text) => text, }); diff --git a/lib/routes/apple/security-releases.ts b/lib/routes/apple/security-releases.ts index 9896caaa6fcf..54f533065322 100644 --- a/lib/routes/apple/security-releases.ts +++ b/lib/routes/apple/security-releases.ts @@ -13,7 +13,7 @@ import { renderDescription } from './templates/security-releases'; export const handler = async (ctx: Context): Promise => { const { language = 'en-us' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://support.apple.com'; const targetUrl: string = new URL(`${language}/100100`, baseUrl).href; diff --git a/lib/routes/aqara/news.ts b/lib/routes/aqara/news.ts index 898ea0dac62d..5de47f531771 100644 --- a/lib/routes/aqara/news.ts +++ b/lib/routes/aqara/news.ts @@ -13,7 +13,7 @@ export const route: Route = { }; async function handler(ctx) { - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 35; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 35; const rootUrl = 'https://www.aqara.cn'; const currentUrl = new URL('news', rootUrl).href; @@ -23,7 +23,7 @@ async function handler(ctx) { const $ = load(response); let items = response - .match(/(parm\.newsTitle[\S\s]*?arr\.push\(parm\))/g) + .match(/(parm\.newsTitle[\s\S]*?arr\.push\(parm\))/g) .slice(0, limit) .map((item) => ({ title: item.match(/parm\.newsTitle = '(.*?)'/)[1], @@ -47,7 +47,7 @@ async function handler(ctx) { ) ); - const icon = $('link[rel="shortcut icon"]').prop('href').split('?')[0]; + const icon = $('link[rel="shortcut icon"]').prop('href').split('?', 1)[0]; return { item: items, diff --git a/lib/routes/aqara/post.tsx b/lib/routes/aqara/post.tsx index 4861ba652fd4..d692b1eb6ac4 100644 --- a/lib/routes/aqara/post.tsx +++ b/lib/routes/aqara/post.tsx @@ -14,7 +14,7 @@ export const route: Route = { }; async function handler(ctx) { - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 50; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 50; const rootUrl = 'https://aqara.com'; const apiSlug = 'wp-json/wp/v2'; diff --git a/lib/routes/arcteryx/regear-new-arrivals.tsx b/lib/routes/arcteryx/regear-new-arrivals.tsx index 6785aa96c410..7394abf81ec0 100644 --- a/lib/routes/arcteryx/regear-new-arrivals.tsx +++ b/lib/routes/arcteryx/regear-new-arrivals.tsx @@ -42,7 +42,7 @@ async function handler() { const data = response.data; const $ = load(data); const contents = $('script:contains("window.__PRELOADED_STATE__")').text(); - const regex = /{.*}/; + const regex = /\{.*\}/; let items = JSON.parse(contents.match(regex)[0]).shop.items; items = items.filter((item) => item.availableSizes.length !== 0); diff --git a/lib/routes/artstation/user.ts b/lib/routes/artstation/user.ts index 49ef9bd275bd..bd088e835b67 100644 --- a/lib/routes/artstation/user.ts +++ b/lib/routes/artstation/user.ts @@ -45,7 +45,7 @@ async function handler(ctx) { method: 'POST', headers, }); - return tokenResponse.headers.getSetCookie()[0].split(';')[0].split('=')[1]; + return tokenResponse.headers.getSetCookie()[0].split(';', 1)[0].split('=', 2)[1]; }); const { data: userData } = await got(`https://www.artstation.com/users/${handle}/quick.json`, { diff --git a/lib/routes/asiafruitchina/categories.ts b/lib/routes/asiafruitchina/categories.ts index 2d73c4eaeb6c..15367ada3883 100644 --- a/lib/routes/asiafruitchina/categories.ts +++ b/lib/routes/asiafruitchina/categories.ts @@ -13,7 +13,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { category = 'all' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '10', 10); + const limit = Number(ctx.req.query('limit') ?? '10'); const baseUrl = 'https://asiafruitchina.net'; const targetUrl: string = new URL(`categories?gspx=${category}`, baseUrl).href; diff --git a/lib/routes/asiafruitchina/news.ts b/lib/routes/asiafruitchina/news.ts index 9ae47525121d..94a2477ac8fd 100644 --- a/lib/routes/asiafruitchina/news.ts +++ b/lib/routes/asiafruitchina/news.ts @@ -12,7 +12,7 @@ import { parseDate } from '@/utils/parse-date'; import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://asiafruitchina.net'; const targetUrl: string = new URL('category/news', baseUrl).href; diff --git a/lib/routes/asiantolick/index.ts b/lib/routes/asiantolick/index.ts index 8034e8d95099..920eedd65eb9 100644 --- a/lib/routes/asiantolick/index.ts +++ b/lib/routes/asiantolick/index.ts @@ -26,7 +26,7 @@ export const route: Route = { async function handler(ctx) { const category = ctx.req.param('category'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 24; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 24; const rootUrl = 'https://asiantolick.com'; const apiUrl = new URL('ajax/buscar_posts.php', rootUrl).href; @@ -64,7 +64,7 @@ async function handler(ctx) { images: image ? [ { - src: image.prop('data-src').split(/\?/)[0], + src: image.prop('data-src').split(/\?/, 1)[0], alt: image.prop('alt'), }, ] @@ -115,7 +115,7 @@ async function handler(ctx) { $ = load(currentResponse); - const title = $('title').text().split(/-/)[0].trim(); + const title = $('title').text().split(/-/, 1)[0].trim(); const icon = $('link[rel="icon"]').first().prop('href'); return { diff --git a/lib/routes/asus/bios.tsx b/lib/routes/asus/bios.tsx index f719b08717dc..25d01b8bc864 100644 --- a/lib/routes/asus/bios.tsx +++ b/lib/routes/asus/bios.tsx @@ -135,7 +135,7 @@ async function handler(ctx) { Size: {item.FileSize}

- Download: {item.DownloadUrl.Global.split('/').pop().split('?')[0]} + Download: {item.DownloadUrl.Global.split('/').pop().split('?', 1)[0]}

) diff --git a/lib/routes/atptour/news.ts b/lib/routes/atptour/news.ts index 062ebff64609..cc27869aad09 100644 --- a/lib/routes/atptour/news.ts +++ b/lib/routes/atptour/news.ts @@ -22,7 +22,7 @@ async function handler(ctx) { const baseUrl = 'https://www.atptour.com'; const favIcon = `${baseUrl}/assets/atptour/assets/favicon.ico`; const { lang = 'en' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const link = `${baseUrl}/${lang}/-/tour/news/latest-filtered-results/0/${limit}`; const { data } = await got(link, { diff --git a/lib/routes/augmentcode/blog.tsx b/lib/routes/augmentcode/blog.tsx index f8ad180f0bef..a0cfba8c5cb9 100644 --- a/lib/routes/augmentcode/blog.tsx +++ b/lib/routes/augmentcode/blog.tsx @@ -31,7 +31,7 @@ const renderDescription = ({ images, description }: { images?: DescriptionImage[ ); export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '50', 10); + const limit = Number(ctx.req.query('limit') ?? '50'); const baseUrl = 'https://augmentcode.com'; const targetUrl: string = new URL('blog', baseUrl).href; diff --git a/lib/routes/auto-stats/index.ts b/lib/routes/auto-stats/index.ts index 6c51ebd95256..36bd61d41de2 100644 --- a/lib/routes/auto-stats/index.ts +++ b/lib/routes/auto-stats/index.ts @@ -30,7 +30,7 @@ export const route: Route = { async function handler(ctx) { const { category = 'xxkd' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const rootUrl = 'http://www.auto-stats.org.cn'; const currentUrl = new URL(`${category}.asp`, rootUrl).href; @@ -51,7 +51,7 @@ async function handler(ctx) { const pubDate = title.match(/(\d{4}(?:\/\d{1,2}){2}\s\d{1,2}(?::\d{2}){2})/)?.[1] ?? undefined; return { - title: title.replace(/●/, '').split(/(\d+/)[0], + title: title.replace(/●/, '').split(/(\d+/, 1)[0], link: new URL(item.parent().prop('href'), rootUrl).href, pubDate: timezone(parseDate(pubDate, 'YYYY/M/D H:mm:ss'), +8), }; diff --git a/lib/routes/azul/packages.ts b/lib/routes/azul/packages.ts index c8ecf5080d49..8a29eb08a8c3 100644 --- a/lib/routes/azul/packages.ts +++ b/lib/routes/azul/packages.ts @@ -7,7 +7,7 @@ import { ViewType } from '@/types'; import ofetch from '@/utils/ofetch'; export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://www.azul.com'; const apiBaseUrl = 'https://api.azul.com'; diff --git a/lib/routes/baai/hub.ts b/lib/routes/baai/hub.ts index e0a39473c331..aebae88db66c 100644 --- a/lib/routes/baai/hub.ts +++ b/lib/routes/baai/hub.ts @@ -8,7 +8,7 @@ import ofetch from '@/utils/ofetch'; import { apiHost, baseUrl, getTagsData, parseEventDetail, parseItem } from './utils'; export const route: Route = { - path: ['/hub/:tagId?/:sort?/:range?'], + path: '/hub/:tagId?/:sort?/:range?', categories: ['programming'], example: '/baai/hub', parameters: { diff --git a/lib/routes/bandcamp/live.ts b/lib/routes/bandcamp/live.ts index a6c3ef81020e..03024db9f3a5 100644 --- a/lib/routes/bandcamp/live.ts +++ b/lib/routes/bandcamp/live.ts @@ -49,7 +49,7 @@ async function handler() { link: item.find('.title-link').attr('href'), title: item.find('.show-title').text(), author: item.find('.show-artist').text(), - pubDate: parseDate(item.find('.show-time-container').text().trim().split(' UTC')[0]), + pubDate: parseDate(item.find('.show-time-container').text().trim().split(' UTC', 1)[0]), description: ` ( async function handler(ctx) { const { sort = '1' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const response = await ofetch('https://app.bc3ts.net/post/list/v2', { headers: { @@ -63,7 +63,7 @@ async function handler(ctx) { }); const items = response.data.map((p) => ({ - title: p.title ?? p.content.split('\n')[0], + title: p.title ?? p.content.split('\n', 1)[0], description: p.content.replaceAll('\n', '
') + (p.media.length && renderMedia(p.media)), link: `${baseUrl}/post/${p.id}`, author: p.user.name, diff --git a/lib/routes/bdys/index.tsx b/lib/routes/bdys/index.tsx index 45c164ea937f..b29f2ce2f3bf 100644 --- a/lib/routes/bdys/index.tsx +++ b/lib/routes/bdys/index.tsx @@ -150,7 +150,7 @@ async function handler(ctx) { }); const downloadResponse = await got({ method: 'get', - url: `${rootUrl}/downloadInfo/list?mid=${item.link.split('/')[4].split('.')[0]}`, + url: `${rootUrl}/downloadInfo/list?mid=${item.link.split('/', 5)[4].split('.', 1)[0]}`, headers, }); const content = load(detailResponse.data); diff --git a/lib/routes/beijingprice/index.ts b/lib/routes/beijingprice/index.ts index d1ade1e7abbe..0422acc01968 100644 --- a/lib/routes/beijingprice/index.ts +++ b/lib/routes/beijingprice/index.ts @@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx) => { const { category = 'jgzx/xwzx' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const rootUrl = 'https://www.beijingprice.cn'; const currentUrl = new URL(category.endsWith('/') ? category : `${category}/`, rootUrl).href; diff --git a/lib/routes/bestblogs/newsletter.ts b/lib/routes/bestblogs/newsletter.ts index 28a7a59095c8..4ce19b5920c2 100644 --- a/lib/routes/bestblogs/newsletter.ts +++ b/lib/routes/bestblogs/newsletter.ts @@ -21,7 +21,7 @@ export const route: Route = { }; async function handler(ctx) { - const pageSize = Number.parseInt(ctx.req.query('limit') ?? '10', 10); + const pageSize = Number(ctx.req.query('limit') ?? '10'); const response = await ofetch('https://www.bestblogs.dev/api/proxy/newsletters', { method: 'GET', diff --git a/lib/routes/bgmlist/onair.tsx b/lib/routes/bgmlist/onair.tsx index f61d9bcd56f7..7f1c2a48e34e 100644 --- a/lib/routes/bgmlist/onair.tsx +++ b/lib/routes/bgmlist/onair.tsx @@ -34,7 +34,7 @@ async function handler(ctx) { item.sites.push({ site: 'dmhy', id: item.titleTranslate['zh-Hans']?.[0] ?? item.title }); const mappedSites = item.sites.map((site) => ({ title: sites[site.site].title, - url: sites[site.site].urlTemplate.replaceAll('{{id}}', site.id), + url: sites[site.site].urlTemplate.replaceAll('{{id}}', () => site.id), begin: site.begin, })); return { diff --git a/lib/routes/bilibili/cache.ts b/lib/routes/bilibili/cache.ts index 213727ec6635..6a86a7fe78a4 100644 --- a/lib/routes/bilibili/cache.ts +++ b/lib/routes/bilibili/cache.ts @@ -21,18 +21,20 @@ const subtitleLimiterQueue = new RateLimiterQueue(subtitleLimiter, { }); const getConfiguredCookie = () => { - if (Object.keys(config.bilibili.cookies).length > 0) { - // Update b_lsid in cookies - for (const key of Object.keys(config.bilibili.cookies)) { - const cookie = config.bilibili.cookies[key]; - if (cookie) { - const updatedCookie = cookie.replace(/b_lsid=[0-9A-F]+_[0-9A-F]+/, `b_lsid=${utils.lsid()}`); - config.bilibili.cookies[key] = updatedCookie; - } - } + if (Object.keys(config.bilibili.cookies).length === 0) { + return; + } - return config.bilibili.cookies[Object.keys(config.bilibili.cookies)[Math.floor(Math.random() * Object.keys(config.bilibili.cookies).length)]] || ''; + // Update b_lsid in cookies + for (const key of Object.keys(config.bilibili.cookies)) { + const cookie = config.bilibili.cookies[key]; + if (cookie) { + const updatedCookie = cookie.replace(/b_lsid=[0-9A-F]+_[0-9A-F]+/, () => `b_lsid=${utils.lsid()}`); + config.bilibili.cookies[key] = updatedCookie; + } } + + return config.bilibili.cookies[Object.keys(config.bilibili.cookies)[Math.floor(Math.random() * Object.keys(config.bilibili.cookies).length)]] || ''; }; const getCookie = (disableConfig = false) => { @@ -50,12 +52,13 @@ const getCookie = (disableConfig = false) => { onBeforeLoad: (page) => { waitForRequest = new Promise((resolve) => { page.on('requestfinished', async (request) => { - if (request.url() === 'https://api.bilibili.com/x/web-interface/nav') { - const cookies = await page.cookies(); - let cookieString = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; '); - cookieString = cookieString.replace(/b_lsid=[0-9A-F]+_[0-9A-F]+/, `b_lsid=${utils.lsid()}`); - resolve(cookieString); + if (request.url() !== 'https://api.bilibili.com/x/web-interface/nav') { + return; } + const cookies = await page.context().cookies(); + let cookieString = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; '); + cookieString = cookieString.replace(/b_lsid=[0-9A-F]+_[0-9A-F]+/, () => `b_lsid=${utils.lsid()}`); + resolve(cookieString); }); }); }, @@ -99,7 +102,7 @@ const getWbiVerifyString = () => { }); const imgUrl = navResponse.data.wbi_img.img_url; const subUrl = navResponse.data.wbi_img.sub_url; - const r = imgUrl.slice(imgUrl.lastIndexOf('/') + 1).split('.')[0] + subUrl.slice(subUrl.lastIndexOf('/') + 1).split('.')[0]; + const r = imgUrl.slice(imgUrl.lastIndexOf('/') + 1).split('.', 1)[0] + subUrl.slice(subUrl.lastIndexOf('/') + 1).split('.', 1)[0]; // const { body: spaceResponse } = await got('https://space.bilibili.com/1', { // headers: { // Referer: 'https://www.bilibili.com/', @@ -117,7 +120,7 @@ const getWbiVerifyString = () => { // 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, // 62, 11, 36, 20, 34, 44, 52, // ]; - const array = JSON.parse(jsResponse.match(/\[(?:\d+,){63}\d+]/)); + const array = JSON.parse(jsResponse.match(/\[(?:\d+,){63}\d+\]/)); const o = []; for (const t of array) { r.charAt(t) && o.push(r.charAt(t)); @@ -350,7 +353,7 @@ const getArticleDataFromCvid = async (cvid, uid) => { const newFormatData = JSON.parse( $('script:contains("window.__INITIAL_STATE__")') .text() - .match(/window\.__INITIAL_STATE__\s*=\s*(.*?);\(/)[1] + .match(/window\.__INITIAL_STATE__\s*=\s*(\S.*?)?;\(/)[1] ); if (newFormatData?.readInfo?.opus?.content?.paragraphs) { diff --git a/lib/routes/bilibili/danmaku.ts b/lib/routes/bilibili/danmaku.ts index a29a8df35585..0f429e617725 100644 --- a/lib/routes/bilibili/danmaku.ts +++ b/lib/routes/bilibili/danmaku.ts @@ -72,9 +72,9 @@ async function handler(ctx) { link, description: `${videoName} 的 弹幕动态`, item: danmakuList.map((item) => ({ - title: `[${processFloatTime(item.p.split(',')[0])}] ${item.text}`, - pubDate: new Date(item.p.split(',')[4] * 1000).toUTCString(), - guid: `${cid}-${item.p.split(',')[4]}-${item.p.split(',')[7]}`, + title: `[${processFloatTime(item.p.split(',', 1)[0])}] ${item.text}`, + pubDate: new Date(item.p.split(',', 5)[4] * 1000).toUTCString(), + guid: `${cid}-${item.p.split(',', 5)[4]}-${item.p.split(',', 8)[7]}`, link, })), }; diff --git a/lib/routes/bilibili/dynamic.ts b/lib/routes/bilibili/dynamic.ts index 3510eaf135bb..9d236d551d1a 100644 --- a/lib/routes/bilibili/dynamic.ts +++ b/lib/routes/bilibili/dynamic.ts @@ -346,14 +346,14 @@ async function handler(ctx) { const emoji = node.emoji; description = description.replaceAll( emoji.text, - `${emoji.text}` + () => + `${emoji.text}` ); } // 处理转发带图评论的情况 if (node?.pics?.length) { const { pics, text } = node; - description = description.replaceAll( - text, + description = description.replaceAll(text, () => pics .map( (pic) => diff --git a/lib/routes/bilibili/fav.ts b/lib/routes/bilibili/fav.ts index f1d104548022..1cf631e7be6a 100644 --- a/lib/routes/bilibili/fav.ts +++ b/lib/routes/bilibili/fav.ts @@ -40,13 +40,13 @@ async function handler(ctx) { throw new Error(message ?? code); } - const userName = data.info.upper.name; + const username = data.info.upper.name; const favName = data.info.title; return { - title: `${userName} 的 bilibili 收藏夹 ${favName}`, + title: `${username} 的 bilibili 收藏夹 ${favName}`, link: `https://space.bilibili.com/${uid}/#/favlist?fid=${fid}`, - description: `${userName} 的 bilibili 收藏夹 ${favName}`, + description: `${username} 的 bilibili 收藏夹 ${favName}`, item: data.medias && diff --git a/lib/routes/bilibili/followings-dynamic.ts b/lib/routes/bilibili/followings-dynamic.ts index 45d600558a6f..566e5da1aed2 100644 --- a/lib/routes/bilibili/followings-dynamic.ts +++ b/lib/routes/bilibili/followings-dynamic.ts @@ -179,7 +179,7 @@ async function handler(ctx) { for (const item of emoji) { data_content = data_content.replaceAll( new RegExp(`\\${item.text}`, 'g'), - `${item.text}` + () => `${item.text}` ); } } diff --git a/lib/routes/bilibili/live-area.ts b/lib/routes/bilibili/live-area.ts index bce9e945e643..40c356d037de 100644 --- a/lib/routes/bilibili/live-area.ts +++ b/lib/routes/bilibili/live-area.ts @@ -47,7 +47,6 @@ async function handler(ctx) { }); let parentTitle = ''; - let parentID: string; let areaTitle = ''; let areaLink = ''; @@ -55,7 +54,7 @@ async function handler(ctx) { for (const area of parentArea.list) { if (area.id === areaID) { parentTitle = parentArea.name; - parentID = parentArea.id; + const parentID: string = parentArea.id; areaTitle = area.name; // cateID = area.cate_id; areaLink = `https://live.bilibili.com/p/eden/area-tags?parentAreaId=${parentID}&areaId=${areaID}`; diff --git a/lib/routes/bilibili/live-room.ts b/lib/routes/bilibili/live-room.ts index 800b4c83f63a..49f25b6a876f 100644 --- a/lib/routes/bilibili/live-room.ts +++ b/lib/routes/bilibili/live-room.ts @@ -34,7 +34,7 @@ async function handler(ctx) { let roomID = ctx.req.param('roomID'); // 短号查询长号 - if (Number.parseInt(roomID, 10) < 10000) { + if (Number(roomID) < 10000) { roomID = await cache.getLiveIDFromShortID(roomID); } const info = await cache.getUserInfoFromLiveID(roomID); diff --git a/lib/routes/bilibili/message-sessions.ts b/lib/routes/bilibili/message-sessions.ts index f2299d2e40fe..4ed8e5877069 100644 --- a/lib/routes/bilibili/message-sessions.ts +++ b/lib/routes/bilibili/message-sessions.ts @@ -50,7 +50,7 @@ interface SessionItem { ack_ts: number; session_ts: number; unread_count: number; - last_msg: { + last_msg: null | { sender_uid: number; receiver_type: number; receiver_id: number; @@ -63,7 +63,7 @@ interface SessionItem { msg_status: number; notify_code: string; msg_source: number; - } | null; + }; group_type: number; can_fold: number; status: number; diff --git a/lib/routes/bilibili/user-channel.ts b/lib/routes/bilibili/user-channel.ts index 4e8ba7ace767..b4e17b8b0e5c 100644 --- a/lib/routes/bilibili/user-channel.ts +++ b/lib/routes/bilibili/user-channel.ts @@ -51,7 +51,7 @@ async function handler(ctx) { if (!channelInfo) { return notFoundData; } - const [userName, face] = await cacheIn.getUsernameAndFaceFromUID(uid); + const [username, face] = await cacheIn.getUsernameAndFaceFromUID(uid); const host = `https://api.bilibili.com/x/series/archives?mid=${uid}&series_id=${sid}&only_normal=true&sort=desc&pn=1&ps=${limit}`; const response = await got(host, { @@ -66,9 +66,9 @@ async function handler(ctx) { } return { - title: `${userName} 的 bilibili 频道 ${channelInfo.meta.name}`, + title: `${username} 的 bilibili 频道 ${channelInfo.meta.name}`, link, - description: `${userName} 的 bilibili 频道`, + description: `${username} 的 bilibili 频道`, image: face, logo: face, icon: face, @@ -77,7 +77,7 @@ async function handler(ctx) { description: utils.renderUGCDescription(embed, item.pic, '', item.aid, undefined, item.bvid), pubDate: parseDate(item.pubdate, 'X'), link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, - author: userName, + author: username, })), }; } diff --git a/lib/routes/bilibili/user-collection.ts b/lib/routes/bilibili/user-collection.ts index fa09627dc240..29b3110189b5 100644 --- a/lib/routes/bilibili/user-collection.ts +++ b/lib/routes/bilibili/user-collection.ts @@ -43,7 +43,7 @@ async function handler(ctx) { const limit = ctx.req.query('limit') ?? 25; const link = `https://space.bilibili.com/${uid}/channel/collectiondetail?sid=${sid}`; - const [userName, face] = await cache.getUsernameAndFaceFromUID(uid); + const [username, face] = await cache.getUsernameAndFaceFromUID(uid); const host = `https://api.bilibili.com/x/polymer/web-space/seasons_archives_list?mid=${uid}&season_id=${sid}&sort_reverse=${sortReverse}&page_num=${page}&page_size=${limit}`; const response = await got(host, { @@ -58,9 +58,9 @@ async function handler(ctx) { } return { - title: `${userName} 的 bilibili 合集 ${data.meta.name}`, + title: `${username} 的 bilibili 合集 ${data.meta.name}`, link, - description: `${userName} 的 bilibili 合集`, + description: `${username} 的 bilibili 合集`, image: face, logo: face, icon: face, @@ -69,7 +69,7 @@ async function handler(ctx) { description: utils.renderUGCDescription(embed, item.pic, '', item.aid, undefined, item.bvid), pubDate: parseDate(item.pubdate, 'X'), link: item.pubdate > utils.bvidTime && item.bvid ? `https://www.bilibili.com/video/${item.bvid}` : `https://www.bilibili.com/video/av${item.aid}`, - author: userName, + author: username, })), }; } diff --git a/lib/routes/bilibili/video.ts b/lib/routes/bilibili/video.ts index 9b2a66bdfae5..c34a821c3b29 100644 --- a/lib/routes/bilibili/video.ts +++ b/lib/routes/bilibili/video.ts @@ -140,7 +140,7 @@ async function applyCookie(page: Page, cookie: string) { .filter((item) => item !== undefined); if (cookies.length > 0) { - await page.setCookie(...cookies); + await page.context().addCookies(cookies); } } @@ -184,9 +184,9 @@ async function fetchVideoListFromBrowser(uid: string): Promise { await applyCookie(page, cookie); } - await page.setRequestInterception(true); - page.on('request', (request) => { - allowedBrowserRequestTypes.has(request.resourceType()) ? request.continue() : request.abort(); + await page.route('**/*', (route) => { + const request = route.request(); + allowedBrowserRequestTypes.has(request.resourceType()) ? route.continue() : route.abort(); }); }, gotoConfig: { waitUntil: 'domcontentloaded' }, diff --git a/lib/routes/bilibili/wasm-exec.ts b/lib/routes/bilibili/wasm-exec.ts index b4359a3da0fc..b1104767e560 100644 --- a/lib/routes/bilibili/wasm-exec.ts +++ b/lib/routes/bilibili/wasm-exec.ts @@ -1,8 +1,16 @@ // oxlint-disable unicorn/prefer-math-trunc +// oxlint-disable unicorn-js/no-this-outside-of-class +// oxlint-disable unicorn-js/no-array-from-fill +// oxlint-disable unicorn-js/no-global-object-property-assignment +// oxlint-disable unicorn-js/no-unnecessary-global-this +// oxlint-disable unicorn-js/no-undeclared-class-members +// oxlint-disable unicorn-js/prefer-array-from-map +// oxlint-disable unicorn-js/require-array-sort-compare +// oxlint-disable unicorn-js/prefer-short-arrow-method // oxlint-disable no-unused-vars +// oxlint-disable unicorn/consistent-function-scoping /* eslint-disable prefer-rest-params */ /* eslint-disable default-case */ -/* eslint-disable unicorn/consistent-function-scoping */ /* eslint-disable no-console */ // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style @@ -634,7 +642,7 @@ _makeFuncWrapper(id) { // somehow avoiding aliasing this with an arrow function doesn't work - // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias + // oxlint-disable-next-line unicorn/no-this-assignment typescript/no-this-alias const go = this; return function () { const event = { id, this: this, args: arguments }; diff --git a/lib/routes/binance/announcement.ts b/lib/routes/binance/announcement.ts index 3600d9d27cde..7c2b0dbeac5c 100644 --- a/lib/routes/binance/announcement.ts +++ b/lib/routes/binance/announcement.ts @@ -18,9 +18,9 @@ interface CatalogItem { interface ArticleListResponse { code: string; - data: { + data: null | { catalogs: CatalogItem[]; - } | null; + }; } const TYPE_CATALOG_ID_MAP: Record = { @@ -50,7 +50,7 @@ const handler: Route['handler'] = async (ctx) => { const baseUrl = 'https://www.binance.com'; const rawType = ctx.req.param('type'); const rawLang = ctx.req.param('lang'); - const limit = Number.parseInt(ctx.req.query('limit') ?? '20', 10); + const limit = Number(ctx.req.query('limit') ?? '20'); const pageSize = Number.isNaN(limit) || limit <= 0 ? 20 : limit; let type = rawType; @@ -90,7 +90,7 @@ const handler: Route['handler'] = async (ctx) => { lang: language, }; - const response = (await ofetch(listUrl.toString(), { headers })) as ArticleListResponse; + const response = (await ofetch(listUrl.href, { headers })) as ArticleListResponse; const catalogs = response.data?.catalogs ?? []; const itemsWithDate = catalogs.flatMap((catalog) => diff --git a/lib/routes/bing/search.ts b/lib/routes/bing/search.ts index 85f352e415f1..5faef975aaeb 100644 --- a/lib/routes/bing/search.ts +++ b/lib/routes/bing/search.ts @@ -42,7 +42,7 @@ async function handler(ctx) { }); const url = new URL('https://cn.bing.com/search'); url.search = searchParams.toString(); - const data = await parser.parseURL(url.toString()); + const data = await parser.parseURL(url.href); return { title: data.title, link: data.link, diff --git a/lib/routes/bioone/featured.ts b/lib/routes/bioone/featured.ts index a4bc770e64b9..8afafa0b09d2 100644 --- a/lib/routes/bioone/featured.ts +++ b/lib/routes/bioone/featured.ts @@ -40,7 +40,7 @@ async function handler(ctx) { .toArray() .map((item) => { item = $(item); - const link = item.attr('href').split('?')[0]; + const link = item.attr('href').split('?', 1)[0]; return { title: item.text(), diff --git a/lib/routes/bitget/announcement.ts b/lib/routes/bitget/announcement.ts index 3ac9b20874d3..e9d070c61d13 100644 --- a/lib/routes/bitget/announcement.ts +++ b/lib/routes/bitget/announcement.ts @@ -111,7 +111,7 @@ const handler: Route['handler'] = async (ctx) => { const nextData = JSON.parse($('script#__NEXT_DATA__').text()); dataItem.description = nextData.props.pageProps.details?.content || nextData.props.pageProps.pageInitInfo?.ruleContent || item.content || ''; } catch (error: any) { - if (error.name && (error.name === 'HTTPError' || error.name === 'RequestError' || error.name === 'FetchError')) { + if (error.name && ['HTTPError', 'RequestError', 'FetchError'].includes(error.name)) { dataItem.description = item.content ?? ''; } else { throw error; diff --git a/lib/routes/bjp/apod.ts b/lib/routes/bjp/apod.ts index 5e17a040401e..8fcc8bed6fc6 100644 --- a/lib/routes/bjp/apod.ts +++ b/lib/routes/bjp/apod.ts @@ -51,7 +51,7 @@ async function handler(ctx) { }; }) .toSorted((a, b) => b.pubDate - a.pubDate) - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10); + .slice(0, ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10); const items = await Promise.all( list.map((e) => diff --git a/lib/routes/bjsk/index.ts b/lib/routes/bjsk/index.ts index 11bf8d02db69..e5deceeea0ae 100644 --- a/lib/routes/bjsk/index.ts +++ b/lib/routes/bjsk/index.ts @@ -55,7 +55,8 @@ async function handler(ctx) { item.description = $('.article-main').html(); item.author = $('.info') .text() - .match(/作者:(.*)\s+来源/)[1]; + .match(/作者:(.*?)来源/)[1] + .trim(); return item; }) ) diff --git a/lib/routes/bjtu/gs.ts b/lib/routes/bjtu/gs.ts index dec0963cf217..4711d23ba439 100644 --- a/lib/routes/bjtu/gs.ts +++ b/lib/routes/bjtu/gs.ts @@ -130,7 +130,7 @@ const getItem = (item, selector) => { const newsDate = item .find('span') .text() - .match(/\d{4}(-|\/|.)\d{1,2}\1\d{1,2}/)[0]; + .match(/\d{4}(.)\d{1,2}\1\d{1,2}/)[0]; const infoTitle = newsInfo.text(); const link = rootURL + newsInfo.attr('href'); diff --git a/lib/routes/bjwxdxh/index.ts b/lib/routes/bjwxdxh/index.ts index b90d7a8aff02..d8060cc9afcd 100644 --- a/lib/routes/bjwxdxh/index.ts +++ b/lib/routes/bjwxdxh/index.ts @@ -59,7 +59,7 @@ async function handler(ctx) { const content = load(response.data); const info = content('div.info') .text() - .match(/作者:(.*?)\s+发布于:(.*?\s+.*?)\s/); + .match(/作者:(\S*)\s+发布于:(\S*\s+.*?)\s/); item.author = info[1]; item.pubDate = timezone(parseDate(info[2], 'YYYY-MM-DD HH:mm:ss'), +8); item.description = content('div#con').html().trim().replaceAll('\n', ''); diff --git a/lib/routes/bjx/huanbao.ts b/lib/routes/bjx/huanbao.ts index a2e2fc1044fa..509014528be4 100644 --- a/lib/routes/bjx/huanbao.ts +++ b/lib/routes/bjx/huanbao.ts @@ -88,7 +88,7 @@ const fetchPage = (link) => const item = { title: $page('title').text(), - description: pages.reduce((desc, $p) => desc + $p('.cc-article').html(), ''), + description: pages.map(($p) => $p('.cc-article').html() ?? '').join(''), pubDate: timezone(parseDate($page('.cc-headline .box p span').eq(0).text()), +8), link, author: $page('.cc-headline .box p span').eq(1).text(), diff --git a/lib/routes/blizzard/news-cn.ts b/lib/routes/blizzard/news-cn.ts index 34fcea7baa23..e0d38c71c359 100644 --- a/lib/routes/blizzard/news-cn.ts +++ b/lib/routes/blizzard/news-cn.ts @@ -97,7 +97,7 @@ const detailParsers = { }; function getList(category, $) { - return parsers[category] ? parsers[category]($) : []; + return Object.hasOwn(parsers, category) ? parsers[category]($) : []; } async function fetchDetail(item, category) { @@ -113,7 +113,7 @@ async function fetchDetail(item, category) { async function handler(ctx) { const category = ctx.req.param('category') || 'ow'; - if (!categoryNames[category]) { + if (!Object.hasOwn(categoryNames, category)) { throw new Error('Invalid category'); } diff --git a/lib/routes/blizzard/news.ts b/lib/routes/blizzard/news.ts index 9eae841dbfd8..5beb0cd4ba82 100644 --- a/lib/routes/blizzard/news.ts +++ b/lib/routes/blizzard/news.ts @@ -156,10 +156,9 @@ async function handler(ctx) { let rssTitle = ''; const { - data: { - feed: { contentItems: response }, - }, + data: { feed }, } = await got(`${apiUrl}?${getSearchParams(category)}`); + const { contentItems: response } = feed; const list = response.map((item) => { const content = item.properties; diff --git a/lib/routes/blockworks/index.ts b/lib/routes/blockworks/index.ts index e1a5ad6b77c4..e09cac311504 100644 --- a/lib/routes/blockworks/index.ts +++ b/lib/routes/blockworks/index.ts @@ -36,7 +36,7 @@ export const route: Route = { async function handler(ctx): Promise { const rssUrl = 'https://blockworks.co/feed'; const feed = await parser.parseURL(rssUrl); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; // Limit to 20 items const limitedItems = feed.items.slice(0, limit); @@ -46,7 +46,7 @@ async function handler(ctx): Promise { limitedItems .map((item) => ({ ...item, - link: item.link?.split('?')[0], + link: item.link?.split('?', 1)[0], })) .map((item) => cache.tryGet(item.link!, async () => { diff --git a/lib/routes/blogread/index.ts b/lib/routes/blogread/index.ts index c6091c6c17ff..3827985a28dd 100644 --- a/lib/routes/blogread/index.ts +++ b/lib/routes/blogread/index.ts @@ -34,7 +34,7 @@ async function handler() { 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], + pubDate: elem.find('dd').eq(1).text().split('\n', 3)[2], }; }); return { diff --git a/lib/routes/bloomberg/index.ts b/lib/routes/bloomberg/index.ts index cb81d7c7924f..b54ea9296718 100644 --- a/lib/routes/bloomberg/index.ts +++ b/lib/routes/bloomberg/index.ts @@ -28,7 +28,7 @@ export const route: Route = { parameters: { site: { description: 'Site ID, can be found below', - options: Object.keys(siteTitleMapping).map((key) => ({ value: key, label: siteTitleMapping[key] })), + options: Object.entries(siteTitleMapping).map(([key, value]) => ({ value: key, label: value })), }, }, features: { diff --git a/lib/routes/bloomberg/utils.ts b/lib/routes/bloomberg/utils.ts index 215c3b716a57..3237d0bf604a 100644 --- a/lib/routes/bloomberg/utils.ts +++ b/lib/routes/bloomberg/utils.ts @@ -55,7 +55,7 @@ const apiEndpoints = { }, }; -const pageTypeRegex1 = /\/(?[\w-]*?)\/(?\d{4}-\d{2}-\d{2}\/.*)/; +const pageTypeRegex1 = /\/(?[\w-]*)\/(?\d{4}-\d{2}-\d{2}\/.*)/; const pageTypeRegex2 = /(?features\/|graphics\/)(?.*)/; const regex = [pageTypeRegex1, pageTypeRegex2]; @@ -101,7 +101,7 @@ const parseArticle = (item) => .map((a) => a && a.groups)[0]; if (group) { const { page, link } = group; - if (apiEndpoints[page]) { + if (Object.hasOwn(apiEndpoints, page)) { const api = { ...apiEndpoints[page] }; let res; @@ -110,7 +110,7 @@ const parseArticle = (item) => res = await redirectGot(apiUrl); } catch (error) { // fallback - if (error.name && (error.name === 'HTTPError' || error.name === 'RequestError' || error.name === 'FetchError')) { + if (error.name && ['HTTPError', 'RequestError', 'FetchError'].includes(error.name)) { try { res = await redirectGot(item.link); } catch { @@ -224,7 +224,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' || error.name === 'FetchError')) { + if (error.name && ['HTTPError', 'RequestError', 'FetchError'].includes(error.name)) { return { title: item.title, link: item.link, @@ -271,7 +271,8 @@ const processLedeMedia = async (story_json) => { video: kind === 'video' && (await processVideo(story_json.ledeAttachment.bmmrId)), }; return renderLedeMedia(media); - } else if (story_json.lede) { + } + if (story_json.lede) { const lede = story_json.lede; const image = { src: lede.url, @@ -280,7 +281,8 @@ const processLedeMedia = async (story_json) => { credit: lede.credit?.replaceAll(capRegex, '') ?? '', }; return renderImageFigure(image); - } else if (story_json.imageAttachments) { + } + if (story_json.imageAttachments) { const attachment = Object.values(story_json.imageAttachments)[0]; if (attachment) { const image = { @@ -292,7 +294,8 @@ const processLedeMedia = async (story_json) => { return renderImageFigure(image); } return ''; - } else if (story_json.type === 'Lede') { + } + if (story_json.type === 'Lede') { const props = story_json.props; const media = { @@ -372,7 +375,7 @@ const processBody = async (body_html, story_json) => { return $.html(); }; -const processVideo = async (bmmrId, summary) => { +const processVideo = async (bmmrId, summary?) => { const api = `https://www.bloomberg.com/multimedia/api/embed?id=${bmmrId}`; const res = await redirectGot(api); @@ -407,15 +410,16 @@ const nodeRenderers = { paragraph: async (node, nextNode) => `

${await nextNode(node.content)}

`, text: (node) => { const { attributes: attr, value: val } = node; - if (attr?.emphasis && attr?.strong) { + if (attr?.emphasis && attr.strong) { return `${val}`; - } else if (attr?.emphasis) { + } + if (attr?.emphasis) { return `${val}`; - } else if (attr?.strong) { + } + if (attr?.strong) { return `${val}`; - } else { - return val; } + return val; }, 'inline-newsletter': async (node, nextNode) => `
${await nextNode(node.content)}
`, 'inline-recirc': async (node, nextNode) => `
${await nextNode(node.content)}
`, @@ -578,7 +582,7 @@ const nextNode = async (nodes) => { }; const nodeToHtmlString = async (node, obj) => { - if (!node.type || !nodeRenderers[node.type]) { + if (!node.type || !Object.hasOwn(nodeRenderers, node.type)) { return `${node.type}`; } const str = await nodeRenderers[node.type](node, nextNode, obj); diff --git a/lib/routes/bluestacks/release.ts b/lib/routes/bluestacks/release.ts index bf3c823ecf1e..e249a296c352 100644 --- a/lib/routes/bluestacks/release.ts +++ b/lib/routes/bluestacks/release.ts @@ -32,11 +32,11 @@ export const route: Route = { }; async function handler() { - const browser = await playwright(); - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + const context = await playwright(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' ? route.continue() : route.abort(); }); await page.goto(pageUrl, { waitUntil: 'domcontentloaded', @@ -46,7 +46,7 @@ async function handler() { const $ = cheerio.load(res); - const items = $('div h3 a') + const list = $('div h3 a') .toArray() .map((item) => { item = $(item); @@ -56,13 +56,13 @@ async function handler() { }; }); - await Promise.all( - items.map((item) => + const items = await Promise.all( + list.map((item) => cache.tryGet(item.link, async () => { - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' ? route.continue() : route.abort(); }); await page.goto(item.link, { waitUntil: 'domcontentloaded', @@ -79,7 +79,7 @@ async function handler() { ) ); - await browser.close(); + await context.close(); return { title: $('.article__title').text().trim(), diff --git a/lib/routes/bnu/mba.ts b/lib/routes/bnu/mba.ts index 8a2c251bfcd5..9c6ccad967f6 100644 --- a/lib/routes/bnu/mba.ts +++ b/lib/routes/bnu/mba.ts @@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx) => { const { category = 'xwdt' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const rootUrl = 'https://mba.bnu.edu.cn'; const currentUrl = new URL(`${category.replace(/\/$/, '')}/`, rootUrl).href; diff --git a/lib/routes/bookwalker/search.tsx b/lib/routes/bookwalker/search.tsx index 1e2ae1daec68..0ef5ff563112 100644 --- a/lib/routes/bookwalker/search.tsx +++ b/lib/routes/bookwalker/search.tsx @@ -10,7 +10,7 @@ import ofetch from '@/utils/ofetch'; export const handler = async (ctx: Context): Promise => { const { filter = 'order=sell_desc' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '24', 10); + const limit = Number(ctx.req.query('limit') ?? '24'); const baseUrl = 'https://www.bookwalker.com.tw'; const targetUrl: string = new URL(`search?${filter}`, baseUrl).href; diff --git a/lib/routes/booru/mmda.ts b/lib/routes/booru/mmda.ts index 69cc18361fa6..779af1236384 100644 --- a/lib/routes/booru/mmda.ts +++ b/lib/routes/booru/mmda.ts @@ -95,7 +95,7 @@ async function handler(ctx) { statisticsTages.find('li, br, strong').remove(); const statisticsStr = statisticsTages.text(); - const regex = /(?[^\s:]+)\s*:\s*(?.+)/gm; + const regex = /(?[^\s:]+)\s*:\s*(?.+)/g; const result = {}; for (const match of statisticsStr.matchAll(regex)) { const { key, value } = match.groups ?? ({} as { key: string; value: string }); diff --git a/lib/routes/bossdesign/index.ts b/lib/routes/bossdesign/index.ts index c76f02563cbe..c5d51514e77c 100644 --- a/lib/routes/bossdesign/index.ts +++ b/lib/routes/bossdesign/index.ts @@ -26,7 +26,7 @@ export const route: Route = { async function handler(ctx) { const category = ctx.req.param('category'); - const limit = Number.parseInt(ctx.req.query('limit'), 10) || undefined; + const limit = Number(ctx.req.query('limit')) || undefined; const baseUrl = 'https://www.bossdesign.cn'; const currentCategory = await cache.tryGet(`bossdesign:categories:${category}`, async () => { diff --git a/lib/routes/bse/index.ts b/lib/routes/bse/index.ts index 6154cf51cdc6..0add93200213 100644 --- a/lib/routes/bse/index.ts +++ b/lib/routes/bse/index.ts @@ -183,7 +183,7 @@ async function handler(ctx) { }, }); - const data = JSON.parse(response.data.match(/null\(\[({.*})]\)/)[1]); + const data = JSON.parse(response.data.match(/null\(\[(\{.*\})\]\)/)[1]); let items: DataItem[]; diff --git a/lib/routes/bsky/feeds.ts b/lib/routes/bsky/feeds.ts index f13660c5c5b4..458c15747374 100644 --- a/lib/routes/bsky/feeds.ts +++ b/lib/routes/bsky/feeds.ts @@ -10,7 +10,7 @@ export const route: Route = { path: '/profile/:handle/feed/:space/:routeParams?', categories: ['social-media'], view: ViewType.SocialMedia, - example: '/bsky.app/profile/jaz.bsky.social/feed/cv:cat', + example: '/bsky/profile/jaz.bsky.social/feed/cv:cat', parameters: { handle: 'User handle, can be found in URL', space: 'Space ID, can be found in URL', @@ -38,7 +38,7 @@ async function handler(ctx) { const feeds = await getFeed(uri, cache.tryGet); const items = feeds.feed.map(({ post }) => ({ - title: post.record.text.split('\n')[0], + title: post.record.text.split('\n', 1)[0], description: renderPost({ text: post.record.text.replaceAll('\n', '
'), embed: post.embed, @@ -46,7 +46,7 @@ async function handler(ctx) { }), author: post.author.displayName, pubDate: parseDate(post.record.createdAt), - link: `https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('app.bsky.feed.post/')[1]}`, + link: `https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('app.bsky.feed.post/', 2)[1]}`, upvotes: post.likeCount, comments: post.replyCount, })); diff --git a/lib/routes/bsky/posts.ts b/lib/routes/bsky/posts.ts index 27e6a602d59a..98f6efcb12d4 100644 --- a/lib/routes/bsky/posts.ts +++ b/lib/routes/bsky/posts.ts @@ -57,7 +57,7 @@ async function handler(ctx) { const authorFeed = await getAuthorFeed(DID, filter, cache.tryGet); const items = authorFeed.feed.map(({ post }) => ({ - title: post.record.text.split('\n')[0], + title: post.record.text.split('\n', 1)[0], description: renderPost({ text: post.record.text.replaceAll('\n', '
'), embed: post.embed, @@ -65,7 +65,7 @@ async function handler(ctx) { }), author: post.author.displayName, pubDate: parseDate(post.record.createdAt), - link: `https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('app.bsky.feed.post/')[1]}`, + link: `https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('app.bsky.feed.post/', 2)[1]}`, upvotes: post.likeCount, comments: post.replyCount, })); diff --git a/lib/routes/bsky/templates/post.tsx b/lib/routes/bsky/templates/post.tsx index 8d03535ed19b..4f7cf19a3ebc 100644 --- a/lib/routes/bsky/templates/post.tsx +++ b/lib/routes/bsky/templates/post.tsx @@ -17,11 +17,11 @@ type VideoEmbed = { playlist?: string; }; -type Embed = { +type Embed = VideoEmbed & { $type?: string; images?: ImageEmbed[]; external?: ExternalEmbed; -} & VideoEmbed; +}; type PostProps = { text?: string; diff --git a/lib/routes/bt0/util.ts b/lib/routes/bt0/util.ts index 9892befcb460..405916add713 100644 --- a/lib/routes/bt0/util.ts +++ b/lib/routes/bt0/util.ts @@ -33,7 +33,7 @@ const genSize = (sizeStr) => { return 0; } - const value = Number.parseFloat(match[1]); + const value = Number(match[1]); const unit = match[3].toUpperCase(); let bytes; diff --git a/lib/routes/bugzilla/bug.ts b/lib/routes/bugzilla/bug.ts index 830944edb0ac..8ca7b125168d 100644 --- a/lib/routes/bugzilla/bug.ts +++ b/lib/routes/bugzilla/bug.ts @@ -37,7 +37,7 @@ async function handler(ctx: Context): Promise { } function markdownFrom(instances: Map, separator: string = ', '): string { - return [...instances.entries()].map(([k, v]) => `[\`${k}\`](https://${v})`).join(separator); + return [...instances].map(([k, v]) => `[\`${k}\`](https://${v})`).join(separator); } export const route: Route = { diff --git a/lib/routes/bullionvault/gold-news.ts b/lib/routes/bullionvault/gold-news.ts index 04d8393a95e8..ff2cc9d09f05 100644 --- a/lib/routes/bullionvault/gold-news.ts +++ b/lib/routes/bullionvault/gold-news.ts @@ -11,7 +11,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const { category } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://bullionvault.com'; const targetUrl: string = new URL(`gold-news${category ? `/${category}` : ''}`, baseUrl).href; diff --git a/lib/routes/c114/roll.ts b/lib/routes/c114/roll.ts index 76fbc4b1a1b5..9ed0d5187ad0 100644 --- a/lib/routes/c114/roll.ts +++ b/lib/routes/c114/roll.ts @@ -9,7 +9,7 @@ import timezone from '@/utils/timezone'; export const handler = async (ctx) => { const { original = 'false' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const rootUrl = 'https://www.c114.com.cn'; const currentUrl = new URL(`news/roll.asp${original === 'true' ? '?o=true' : ''}`, rootUrl).href; diff --git a/lib/routes/caam/index.ts b/lib/routes/caam/index.ts index 9753cb9086f4..79090a018c82 100644 --- a/lib/routes/caam/index.ts +++ b/lib/routes/caam/index.ts @@ -14,7 +14,7 @@ export const route: Route = { async function handler(ctx) { const { category = '1' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const rootUrl = 'http://www.caam.org.cn'; const currentUrl = new URL(`chn/1/cate_${category}/list_1.html`, rootUrl).href; diff --git a/lib/routes/caixin/blog.ts b/lib/routes/caixin/blog.ts index 0a35b557dbd3..ce118899bf79 100644 --- a/lib/routes/caixin/blog.ts +++ b/lib/routes/caixin/blog.ts @@ -41,7 +41,7 @@ async function handler(ctx) { const user = $('div.indexMainConri > script[type="text/javascript"]') .text() .slice('window.user = '.length + 1) - .split(';')[0] + .split(';', 1)[0] .replaceAll(/\s/g, ''); const authorId = user.match(/id:"(\d+)"/)[1]; const authorName = user.match(/name:"(.*?)"/)[1]; @@ -77,28 +77,27 @@ async function handler(ctx) { image: avatar, item: items, }; - } else { - const { data } = await got('https://blog.caixin.com/blog-api/post/index', { - searchParams: { - page: 1, - size: limit, - }, - }); - const posts = data.data.map((item) => ({ - title: item.title, - description: item.brief, - author: item.authorName, - link: item.postUrl.replace('http://', 'https://'), - pubDate: parseDate(item.publishTime, 'x'), - })); - const items = await Promise.all(posts.map((item) => cache.tryGet(item.link, () => parseBlogArticle(item)))); - - return { - title: '财新博客 - 全部', - link: 'https://blog.caixin.com', - // description: introduce, - // image: avatar, - item: items, - }; } + const { data } = await got('https://blog.caixin.com/blog-api/post/index', { + searchParams: { + page: 1, + size: limit, + }, + }); + const posts = data.data.map((item) => ({ + title: item.title, + description: item.brief, + author: item.authorName, + link: item.postUrl.replace('http://', 'https://'), + pubDate: parseDate(item.publishTime, 'x'), + })); + const items = await Promise.all(posts.map((item) => cache.tryGet(item.link, () => parseBlogArticle(item)))); + + return { + title: '财新博客 - 全部', + link: 'https://blog.caixin.com', + // description: introduce, + // image: avatar, + item: items, + }; } diff --git a/lib/routes/caixin/category.ts b/lib/routes/caixin/category.ts index 0fd9f0e2072b..9e4e7dc832c0 100644 --- a/lib/routes/caixin/category.ts +++ b/lib/routes/caixin/category.ts @@ -60,7 +60,7 @@ async function handler(ctx) { const entity = JSON.parse( $('script') .text() - .match(/var entity = ({.*?})/)[1] + .match(/var entity = (\{.*?\})/)[1] ); const { diff --git a/lib/routes/caixin/utils-fulltext.ts b/lib/routes/caixin/utils-fulltext.ts index e17c54b6b391..af5b02716165 100644 --- a/lib/routes/caixin/utils-fulltext.ts +++ b/lib/routes/caixin/utils-fulltext.ts @@ -14,7 +14,7 @@ export async function getFulltext(url: string) { if (!config.caixin.cookie) { return; } - if (!/(\d+)\.html/.test(url)) { + if (!/\d+\.html/.test(url)) { return; } const articleID = url.match(/(\d+)\.html/)[1]; @@ -24,7 +24,7 @@ export async function getFulltext(url: string) { const userID = config.caixin.cookie .split(';') .find((e) => e.includes('SA_USER_UID')) - ?.split('=')[1]; // + ?.split('=', 2)[1]; // const rawString = `id=${articleID}&uid=${userID}&${nonce}=nonce`; diff --git a/lib/routes/caixin/utils.ts b/lib/routes/caixin/utils.ts index fd9be6dee05c..73e7e547d514 100644 --- a/lib/routes/caixin/utils.ts +++ b/lib/routes/caixin/utils.ts @@ -7,24 +7,23 @@ import { renderArticle } from './templates/article'; const parseArticle = async (item) => { if (new URL(item.link).hostname.endsWith('.blog.caixin.com')) { return parseBlogArticle(item); - } else { - const { data: response } = await got(item.link); - - const $ = load(response); + } + const { data: response } = await got(item.link); - item.description = renderArticle({ - item, - $, - }); + const $ = load(response); - if (item.audio) { - item.itunes_item_image = item.audio_image_url; - item.enclosure_url = item.audio; - item.enclosure_type = 'audio/mpeg'; - } + item.description = renderArticle({ + item, + $, + }); - return item; + if (item.audio) { + item.itunes_item_image = item.audio_image_url; + item.enclosure_url = item.audio; + item.enclosure_type = 'audio/mpeg'; } + + return item; }; const parseBlogArticle = async (item) => { diff --git a/lib/routes/caixin/weekly.ts b/lib/routes/caixin/weekly.ts index c67ef5b04c12..89d1c9596ed8 100644 --- a/lib/routes/caixin/weekly.ts +++ b/lib/routes/caixin/weekly.ts @@ -37,7 +37,7 @@ async function handler(ctx) { .map((item) => ({ link: $(item).attr('href'), })), - ].slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10) as DataItem[]; + ].slice(0, ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10) as DataItem[]; const items = (await Promise.all( list.map((item) => diff --git a/lib/routes/caixinglobal/latest.ts b/lib/routes/caixinglobal/latest.ts index 87b02b2384cb..1df291e168a3 100644 --- a/lib/routes/caixinglobal/latest.ts +++ b/lib/routes/caixinglobal/latest.ts @@ -34,7 +34,7 @@ async function handler(ctx) { searchParams: { subject: '100990318;100990314;100990311', start: 0, - count: ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20, + count: ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20, type: '2', _: Date.now(), }, diff --git a/lib/routes/cankaoxiaoxi/index.tsx b/lib/routes/cankaoxiaoxi/index.tsx index 2f29816ffd3e..9eb373540064 100644 --- a/lib/routes/cankaoxiaoxi/index.tsx +++ b/lib/routes/cankaoxiaoxi/index.tsx @@ -87,7 +87,7 @@ async function handler(ctx) { const data = detailResponse.data; - item.link = `${rootUrl}/#/detailsPage/${id}/${data.id}/1/${data.publishTime.split(' ')[0]}`; + item.link = `${rootUrl}/#/detailsPage/${id}/${data.id}/1/${data.publishTime.split(' ', 1)[0]}`; item.description = data.txt; } diff --git a/lib/routes/capitalmind/utils.ts b/lib/routes/capitalmind/utils.ts index dc04f8b14e28..08fb49362067 100644 --- a/lib/routes/capitalmind/utils.ts +++ b/lib/routes/capitalmind/utils.ts @@ -24,7 +24,7 @@ export async function fetchArticles(path) { .text() .trim(); const image = $element.find('img').attr('src'); - const imageUrl = image?.startsWith('/_next/image') ? image.split('url=')[1].split('&')[0] : image; + const imageUrl = image?.startsWith('/_next/image') ? image.split('url=', 2)[1].split('&', 1)[0] : image; const decodedImageUrl = imageUrl ? decodeURIComponent(imageUrl) : ''; // Fetch full article content diff --git a/lib/routes/cara/likes.ts b/lib/routes/cara/likes.ts index e877cf30eb91..d69b7b89d749 100644 --- a/lib/routes/cara/likes.ts +++ b/lib/routes/cara/likes.ts @@ -7,7 +7,7 @@ import type { PostsResponse } from './types'; import { customFetch, parseUserData } from './utils'; export const route: Route = { - path: ['/likes/:user'], + path: '/likes/:user', categories: ['social-media'], example: '/cara/likes/fengz', parameters: { user: 'username' }, @@ -24,7 +24,7 @@ export const route: Route = { async function handler(ctx): Promise { const user = ctx.req.param('user'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const userInfo = await parseUserData(user); const api = `${API_HOST}/posts/getAllLikesByUser?slug=${userInfo.slug}&take=${limit}`; diff --git a/lib/routes/cara/portfolio.ts b/lib/routes/cara/portfolio.ts index 48e57f2f7bdd..89cae98773a6 100644 --- a/lib/routes/cara/portfolio.ts +++ b/lib/routes/cara/portfolio.ts @@ -6,7 +6,7 @@ import type { PortfolioResponse } from './types'; import { customFetch, fetchPortfolioItem, parseUserData } from './utils'; export const route: Route = { - path: ['/portfolio/:user'], + path: '/portfolio/:user', categories: ['social-media'], example: '/cara/portfolio/fengz', parameters: { user: 'username' }, @@ -23,7 +23,7 @@ export const route: Route = { async function handler(ctx): Promise { const user = ctx.req.param('user'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const userInfo = await parseUserData(user); const api = `${API_HOST}/profiles/portfolio?id=${userInfo.id}&take=${limit}`; diff --git a/lib/routes/cara/timeline.ts b/lib/routes/cara/timeline.ts index 523d0b1008b8..283ec9113593 100644 --- a/lib/routes/cara/timeline.ts +++ b/lib/routes/cara/timeline.ts @@ -7,7 +7,7 @@ import type { PostsResponse } from './types'; import { customFetch, parseUserData } from './utils'; export const route: Route = { - path: ['/timeline/:user'], + path: '/timeline/:user', categories: ['social-media'], example: '/cara/timeline/fengz', parameters: { user: 'username' }, @@ -24,7 +24,7 @@ export const route: Route = { async function handler(ctx): Promise { const user = ctx.req.param('user'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const userInfo = await parseUserData(user); const api = `${API_HOST}/posts/getAllByUser?slug=${userInfo.slug}&take=${limit}`; diff --git a/lib/routes/carousell/index.ts b/lib/routes/carousell/index.ts index 8657d8853759..f1b39f03aeda 100644 --- a/lib/routes/carousell/index.ts +++ b/lib/routes/carousell/index.ts @@ -265,7 +265,7 @@ async function handler(ctx): Promise { const siteResponse = await ofetch.raw(baseUrl); const cookies = siteResponse.headers .getSetCookie() - ?.map((c) => c.split(';')[0]) + ?.map((c) => c.split(';', 1)[0]) .join('; '); const csrfToken = siteResponse._data.match(/"csrfToken":"(.*?)","/)[1]; diff --git a/lib/routes/cas/iee/kydt.ts b/lib/routes/cas/iee/kydt.ts index 8d5e23ffc15e..a777b1ebf344 100644 --- a/lib/routes/cas/iee/kydt.ts +++ b/lib/routes/cas/iee/kydt.ts @@ -60,7 +60,7 @@ async function handler() { const content = load(detailResponse.data); item.description = content('.article-content').html(); - item.pubDate = timezone(parseDate(content('time').text().split(':')[1]), 8); + item.pubDate = timezone(parseDate(content('time').text().split(':', 2)[1]), 8); return item; }) diff --git a/lib/routes/cas/sim/kyjz.ts b/lib/routes/cas/sim/kyjz.ts index 116fc6ccb316..1e61deea23fd 100644 --- a/lib/routes/cas/sim/kyjz.ts +++ b/lib/routes/cas/sim/kyjz.ts @@ -55,7 +55,7 @@ async function handler() { const $ = load(response.data); const author = $('.qtinfo.hidden-lg.hidden-md.hidden-sm').text(); - const reg = /文章来源:(.*?)\|/g; + const reg = /文章来源:(.*?)\|/; item.title = $('p.wztitle').text().trim(); item.author = reg.exec(author)[1].toString().trim(); diff --git a/lib/routes/casssp/news.ts b/lib/routes/casssp/news.ts index ec9e359fbb69..65a36ed31a70 100644 --- a/lib/routes/casssp/news.ts +++ b/lib/routes/casssp/news.ts @@ -28,7 +28,7 @@ export const route: Route = { async function handler(ctx) { const { category = '3' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const rootUrl = 'http://www.casssp.org.cn'; const currentUrl = new URL(`news/${category}/`, rootUrl).href; diff --git a/lib/routes/castbox/channel.ts b/lib/routes/castbox/channel.ts index cfda05902718..57940fe64d9a 100644 --- a/lib/routes/castbox/channel.ts +++ b/lib/routes/castbox/channel.ts @@ -9,7 +9,7 @@ const PERMUTAION_MAP = [24, 13, 4, 19, 6, 0, 8, 21, 25, 7, 28, 1, 15, 31, 10, 9, const getNonce = (params: Record) => { const m = new Date().toISOString().slice(0, 10).replaceAll('-', ''); - const sortedKeys = Object.keys(params).toSorted(); + const sortedKeys = Object.keys(params).toSorted((a, b) => a.localeCompare(b)); const queryParts = sortedKeys.map((k) => `${k}=${params[k]}`); const queryStr = queryParts.join('&'); @@ -50,7 +50,7 @@ You can use the RSSHub global \`limit\` query parameter to specify the maximum n maintainers: ['ananyatimalsina'], handler: async (ctx) => { const { channel } = ctx.req.param(); - const cid = channel.split('-id')[1]; + const cid = channel.split('-id', 2)[1]; if (!cid) { throw new Error('Invalid channel format. Missing -id'); @@ -66,7 +66,7 @@ You can use the RSSHub global \`limit\` query parameter to specify the maximum n } const chData = channelData.data; - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit') as string, 10) : 50; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit') as string) : 50; const epParams = { cid, limit, r: 1, raw: 1, web: 1 }; const { m: em, n: en, queryStr: eQuery } = getNonce(epParams); diff --git a/lib/routes/cbaigui/index.ts b/lib/routes/cbaigui/index.ts index 9a3872640c96..b07d491428fe 100644 --- a/lib/routes/cbaigui/index.ts +++ b/lib/routes/cbaigui/index.ts @@ -16,7 +16,7 @@ export const route: Route = { }; async function handler(ctx) { - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 50; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 50; let filterName; @@ -63,7 +63,7 @@ async function handler(ctx) { content('p img').each((_, el) => { const image = content(el); - const src = image.prop('src').split('!')[0]; + const src = image.prop('src').split('!', 1)[0]; const width = image.prop('width'); const height = image.prop('height'); diff --git a/lib/routes/cbirc/index.ts b/lib/routes/cbirc/index.ts deleted file mode 100644 index 1f6d8e3bd916..000000000000 --- a/lib/routes/cbirc/index.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; - -const categories = { - jgdt: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '监管动态', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=915,pageIndex=1,pageSize=18.json', - title: '监管动态', - }, - ggtz: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '公告通知', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=925,pageIndex=1,pageSize=18.json', - title: '公告通知', - }, - zcfg: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '政策法规', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=926,pageIndex=1,pageSize=18.json', - title: '政策法规', - }, - zcjd: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '政策解读', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=916,pageIndex=1,pageSize=18.json', - title: '政策解读', - }, - zqyj: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '征求意见', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=951,pageIndex=1,pageSize=18.json', - title: '征求意见', - }, - xzxk: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '行政许可', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=930,pageIndex=1,pageSize=18.json', - title: '行政许可', - }, - xzcf: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '行政处罚', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=931,pageIndex=1,pageSize=18.json', - title: '行政处罚', - }, - xzjgcs: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '行政监管措施', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=932,pageIndex=1,pageSize=18.json', - title: '行政监管措施', - }, - gzlw: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '工作论文', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=934,pageIndex=1,pageSize=18.json', - title: '工作论文', - }, - jrzgyj: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '金融监管研究', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=935,pageIndex=1,pageSize=18.json', - title: '金融监管研究', - }, - tjxx: { - baseUrl: 'http://www.cbirc.gov.cn', - description: '统计信息', - link: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectDocByItemIdAndChild/data_itemId=954,pageIndex=1,pageSize=18.json', - title: '统计信息', - }, -}; - -async function getContent(item) { - const response = await got({ - method: 'get', - url: 'http://www.cbirc.gov.cn/cn/static/data/DocInfo/SelectByDocId/data_docId=' + item.docId + '.json', - }); - return response.data.data.docClob; -} - -export const route: Route = { - path: '/:category?', - radar: [ - { - source: ['cbirc.gov.cn/:category', 'cbirc.gov.cn/'], - }, - ], - name: 'Unknown', - maintainers: ['JkCheung'], - handler, -}; - -async function handler(ctx) { - const category = ctx.req.param('category') ?? 'ggtz'; - const cat = categories[category]; - - // 请求集合 - const response = await cache.tryGet(cat.link, async () => { - const resp = await got({ - method: 'get', - url: cat.link, - headers: { - Referer: 'http://www.cbirc.gov.cn', - }, - }); - return resp.data; - }); - - // 遍历数据集合 - const dataLs = await Promise.all( - response.data.rows.map(async (item) => { - const content = await getContent(item); - return { - title: item.docTitle, - // 文章正文 - description: content, - // 文章发布时间 - pubDate: item.publishDate, - // 文章链接 - link: `http://www.cbirc.gov.cn/cn/view/pages/ItemDetail.html?docId=${item.docId}&itemId=925&generaltype=0`, - }; - }) - ); - - return { - title: `中国银保监会-${cat.title}`, - link: cat.link, - description: `中国银保监会-${cat.title}`, - item: dataLs, - language: 'zh-CN', - }; -} diff --git a/lib/routes/cbndata/information.ts b/lib/routes/cbndata/information.ts index 0cbcbb6f2f88..54fff60d859f 100644 --- a/lib/routes/cbndata/information.ts +++ b/lib/routes/cbndata/information.ts @@ -12,7 +12,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { id = 'all' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '50', 10); + const limit = Number(ctx.req.query('limit') ?? '50'); const baseUrl = 'https://www.cbndata.com'; const targetUrl: string = new URL(`information?tag_id=${id}`, baseUrl).href; diff --git a/lib/routes/cbpanet/index.ts b/lib/routes/cbpanet/index.ts index 7aa617d5f230..89c858084d71 100644 --- a/lib/routes/cbpanet/index.ts +++ b/lib/routes/cbpanet/index.ts @@ -8,7 +8,7 @@ import timezone from '@/utils/timezone'; export const handler = async (ctx) => { const { bigId = '2', smallId = '11' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const rootUrl = 'https://www.cbpanet.com'; const currentUrl = new URL(`dzp_news.aspx?bigid=${bigId}&smallid=${smallId}`, rootUrl).href; @@ -73,7 +73,7 @@ export const handler = async (ctx) => { item: items, allowEmpty: true, image, - author: title.split(/-/)[0], + author: title.split(/-/, 1)[0], language, }; }; diff --git a/lib/routes/ccac/news.ts b/lib/routes/ccac/news.ts index cf893ee8e771..508c196ce03a 100644 --- a/lib/routes/ccac/news.ts +++ b/lib/routes/ccac/news.ts @@ -32,21 +32,21 @@ export const route: Route = { }; async function handler(ctx) { - const browser = await playwright(); + const context = await playwright(); const lang = ctx.req.param('lang') ?? 'sc'; const type = utils.TYPE[ctx.req.param('type')]; const BASE = utils.langBase(lang); - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' ? route.continue() : route.abort(); }); await page.goto(BASE, { waitUntil: 'domcontentloaded', }); const articles = await page.evaluate(() => window.articles); - await browser.close(); + await context.close(); const list = utils .typeFilter(articles, type) diff --git a/lib/routes/ccagm/index.ts b/lib/routes/ccagm/index.ts index 62f3bd55aa11..9f56e8f18e7a 100644 --- a/lib/routes/ccagm/index.ts +++ b/lib/routes/ccagm/index.ts @@ -12,7 +12,7 @@ import timezone from '@/utils/timezone'; export const handler = async (ctx: Context): Promise => { const { category = 'association-news' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '10', 10); + const limit = Number(ctx.req.query('limit') ?? '10'); const baseUrl = 'http://www.ccagm.org.cn'; const targetUrl: string = new URL(category, baseUrl).href; diff --git a/lib/routes/cccmc/index.ts b/lib/routes/cccmc/index.ts index 2f5354f8afc3..188447cd7205 100644 --- a/lib/routes/cccmc/index.ts +++ b/lib/routes/cccmc/index.ts @@ -11,7 +11,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const { category = 'ywgg/tzgg' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '15', 10); + const limit = Number(ctx.req.query('limit') ?? '15'); const baseUrl = 'https://www.cccmc.org.cn'; const targetUrl: string = new URL(category.endsWith('/') ? category : `${category}/`, baseUrl).href; @@ -95,7 +95,7 @@ export const handler = async (ctx: Context): Promise => { return { title, - description: title.split(/-/)[0].trim(), + description: title.split(/-/, 1)[0].trim(), link: targetUrl, item: items, allowEmpty: true, diff --git a/lib/routes/ccfa/index.tsx b/lib/routes/ccfa/index.tsx index a497182bee3a..0b9332cc423d 100644 --- a/lib/routes/ccfa/index.tsx +++ b/lib/routes/ccfa/index.tsx @@ -9,7 +9,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx) => { const { type = '1' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const rootUrl = 'http://www.ccfa.org.cn'; const currentUrl = new URL(`portal/cn/xiehui_list.jsp?type=${type}`, rootUrl).href; diff --git a/lib/routes/ccg/index.ts b/lib/routes/ccg/index.ts index d53458f64e56..714c86cbaa6c 100644 --- a/lib/routes/ccg/index.ts +++ b/lib/routes/ccg/index.ts @@ -13,7 +13,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { category = 'news' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '7', 10); + const limit = Number(ctx.req.query('limit') ?? '7'); const baseUrl = 'http://www.ccg.org.cn'; const targetUrl: string = new URL(category, baseUrl).href; diff --git a/lib/routes/ccmn/price-adjustment.tsx b/lib/routes/ccmn/price-adjustment.tsx index 830434cee1c8..09c2bc8225d8 100644 --- a/lib/routes/ccmn/price-adjustment.tsx +++ b/lib/routes/ccmn/price-adjustment.tsx @@ -86,12 +86,12 @@ const READABLE_CATEGORIES = { async function handler(ctx) { const category = ctx.req.param('category'); - const subDomain = SUB_DOMAIN_MAP[category]; - if (!subDomain) { + const subdomain = SUB_DOMAIN_MAP[category]; + if (!subdomain) { throw new Error('未知的金属类型'); } - const url = `https://${subDomain}`; + const url = `https://${subdomain}`; const response = await got({ method: 'get', @@ -133,7 +133,7 @@ async function handler(ctx) { .trim(); const changeStr = $avgSpan.find('.up_down').text().trim(); - const changeNum = Number.parseFloat(changeStr); + const changeNum = Number(changeStr); let icon; // 如果 change 为 0 或者无法解析,则不显示图标 if (changeNum > 0) { diff --git a/lib/routes/cctv/lm.ts b/lib/routes/cctv/lm.ts index f3b856f32f99..e378927a163f 100644 --- a/lib/routes/cctv/lm.ts +++ b/lib/routes/cctv/lm.ts @@ -90,8 +90,12 @@ async function handler(ctx) { item.description += `
`; } - for (let i = 2; data.video[`chapters${i}`]; i++) { - for (const c of data.video[`chapters${i}`]) { + for (let i = 2; ; i++) { + const chapters = data.video[`chapters${i}`]; + if (!chapters) { + break; + } + for (const c of chapters) { item.description += `
`; } } diff --git a/lib/routes/cctv/utils/news.ts b/lib/routes/cctv/utils/news.ts index 14ef0ce7f32d..8c1c87acf3ed 100644 --- a/lib/routes/cctv/utils/news.ts +++ b/lib/routes/cctv/utils/news.ts @@ -45,7 +45,7 @@ const getNews = async (category) => { type = 'PHO'; } else if (id.startsWith('VIDE')) { // 视频 - const vid = path.parse(image).name.split('-')[0]; + const vid = path.parse(image).name.split('-', 1)[0]; api = `https://vdn.apps.cntv.cn/api/getHttpVideoInfo.do?pid=${vid}`; type = 'VIDE'; } else { diff --git a/lib/routes/cctv/utils/xinwen1j1.ts b/lib/routes/cctv/utils/xinwen1j1.ts index a2072272c17d..7122269a864a 100644 --- a/lib/routes/cctv/utils/xinwen1j1.ts +++ b/lib/routes/cctv/utils/xinwen1j1.ts @@ -14,7 +14,7 @@ async function loadContent(link) { // console.log('********') const js_txt = '' + $('script'); - const guid = js_txt.split('guid_Ad_VideoCode = "')[1].split('";')[0]; + const guid = js_txt.split('guid_Ad_VideoCode = "', 2)[1].split('";', 1)[0]; // console.log(guid+' js_txt********') const { data: videoDetail } = await got({ method: 'get', diff --git a/lib/routes/cctv/xwlb.ts b/lib/routes/cctv/xwlb.ts index c22da35122fe..3c2bf56b506e 100644 --- a/lib/routes/cctv/xwlb.ts +++ b/lib/routes/cctv/xwlb.ts @@ -76,15 +76,15 @@ const getXWLB = async () => { description: await cache.tryGet(url, async () => { const res = await got(url); const content = load(res.data); - const list: string[] = []; - content('body li').map((i, elem) => { - const e = content(elem); - const href = e.find('a').attr('href'); - const title = e.find('a').attr('title'); - const dur = e.find('span').text(); - list.push(`${title} ⏱${dur}`); - return i; - }); + const list = content('body li') + .toArray() + .map((elem) => { + const e = content(elem); + const href = e.find('a').attr('href'); + const title = e.find('a').attr('title'); + const dur = e.find('span').text(); + return `${title} ⏱${dur}`; + }); return list.join('
\n'); }), }; diff --git a/lib/routes/cebbank/all.tsx b/lib/routes/cebbank/all.tsx index 66466fcafa69..cd6c59cc263d 100644 --- a/lib/routes/cebbank/all.tsx +++ b/lib/routes/cebbank/all.tsx @@ -62,9 +62,10 @@ async function handler(ctx) { item: items, }; + const pubDate = parseDate($('#t_id span').text().slice(5), 'YYYY-MM-DD HH:mm', true); ctx.set('json', { ...ret, - pubDate: timezone(parseDate($('#t_id span').text().slice(5), 'YYYY-MM-DD HH:mm', true), 0), + pubDate: timezone(pubDate, 0), }); return ret; } diff --git a/lib/routes/cfachina/analygarden.ts b/lib/routes/cfachina/analygarden.ts index 13bf4c7bc463..07f640b2cd4a 100644 --- a/lib/routes/cfachina/analygarden.ts +++ b/lib/routes/cfachina/analygarden.ts @@ -59,7 +59,7 @@ async function handler(ctx) { }, searchParams: { pageNo: 1, - pageSize: ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20, + pageSize: ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20, keyword: '', startTime: '', endTime: '', diff --git a/lib/routes/cfmmc/index.ts b/lib/routes/cfmmc/index.ts index 447ca72c8ec7..e1c5105c1e46 100644 --- a/lib/routes/cfmmc/index.ts +++ b/lib/routes/cfmmc/index.ts @@ -15,7 +15,7 @@ export const route: Route = { async function handler(ctx) { const { id = 'main/noticeannouncement/cfmmcnotice' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10; const rootUrl = 'http://www.cfmmc.com'; const apiUrl = new URL('servlet/json', rootUrl).href; diff --git a/lib/routes/cfr/utils.ts b/lib/routes/cfr/utils.ts index b9dadd7ccd44..9d7fe078a459 100644 --- a/lib/routes/cfr/utils.ts +++ b/lib/routes/cfr/utils.ts @@ -14,7 +14,7 @@ export function getDataItem(href: string) { const link = `${origin}${href}`; return cache.tryGet(link, async () => { - const prefix = href?.split('/')[1]; + const prefix = href?.split('/', 2)[1]; const res = await ofetch(link); const $ = load(res); diff --git a/lib/routes/cgtn/podcast.ts b/lib/routes/cgtn/podcast.ts index 4354e79f2380..4a5c773f58cc 100644 --- a/lib/routes/cgtn/podcast.ts +++ b/lib/routes/cgtn/podcast.ts @@ -45,7 +45,7 @@ async function handler(ctx) { const data = await getData(categoryMap[category], id); const items = data.data.map((item) => ({ title: item.title, - pubDate: combDate(item.showDate, item.time.split(' ')[0]), + pubDate: combDate(item.showDate, item.time.split(' ', 1)[0]), description: item.programSeries.content || item.detail, itunes_item_image: item.programUrl, itunes_duration: item.duration, diff --git a/lib/routes/changba/user.tsx b/lib/routes/changba/user.tsx index 26e6210ebec0..e45b8cbfd15e 100644 --- a/lib/routes/changba/user.tsx +++ b/lib/routes/changba/user.tsx @@ -69,14 +69,14 @@ async function handler(ctx) { return null; } - workid = workid.split("'")[1]; + workid = workid.split("'", 2)[1]; if (!workid) { return null; } const mp3 = `https://upscuw.changba.com/${workid}.mp3`; const description = renderToString(); - const itunes_item_image = $('div.work-cover').attr('style').replace(')', '').split('url(')[1]; + const itunes_item_image = $('div.work-cover').attr('style').replace(')', '').split('url(', 2)[1]; return { title: $('.work-title').text(), description, diff --git a/lib/routes/chiculture/topic.ts b/lib/routes/chiculture/topic.ts index 276df7a1ac48..8edf833f4076 100644 --- a/lib/routes/chiculture/topic.ts +++ b/lib/routes/chiculture/topic.ts @@ -59,13 +59,13 @@ async function handler(ctx) { } else if (item.title.includes('一周時事通識')) { for (const tag of item.pubDate) { if (/^\d{4}年$/.test(tag.title)) { - const monthDayStr = item.title.split('- ')[1] ?? item.title.split('-')[1]; + const monthDayStr = item.title.split('- ', 2)[1] ?? item.title.split('-', 2)[1]; item.pubDate = timezone(parseDate(monthDayStr, 'D/M'), +8); break; } } } else if (/^\d{4}年新聞回顧$/.test(item.title)) { - item.pubDate = parseDate(`${item.title.split('年')[0]}-12-31`); + item.pubDate = parseDate(`${item.title.split('年', 1)[0]}-12-31`); } else { item.pubDate = ''; } diff --git a/lib/routes/chikubi/navigation.ts b/lib/routes/chikubi/navigation.ts index b4304a5b222c..87424f02cb55 100644 --- a/lib/routes/chikubi/navigation.ts +++ b/lib/routes/chikubi/navigation.ts @@ -46,23 +46,22 @@ async function handler(ctx): Promise { link: `${baseUrl}/best-nipple-article`, item: items, }; - } else { - const { url, title } = navigationItems[keyword]; + } + const { url, title } = navigationItems[keyword]; - const feed = await parser.parseURL(`${baseUrl}${url}/feed`); + const feed = await parser.parseURL(`${baseUrl}${url}/feed`); - const list = feed.items.map((item) => ({ - title: item.title, - link: item.link, - })); + const list = feed.items.map((item) => ({ + title: item.title, + link: item.link, + })); - // 獲取內文 - const items = await processItems(list); + // 獲取內文 + const items = await processItems(list); - return { - title: `${title} - chikubi.jp`, - link: `${baseUrl}${url}`, - item: items, - }; - } + return { + title: `${title} - chikubi.jp`, + link: `${baseUrl}${url}`, + item: items, + }; } diff --git a/lib/routes/chikubi/nipple-video-category.ts b/lib/routes/chikubi/nipple-video-category.ts index 5077f504b093..05e1996ddb2b 100644 --- a/lib/routes/chikubi/nipple-video-category.ts +++ b/lib/routes/chikubi/nipple-video-category.ts @@ -44,7 +44,7 @@ async function handler(ctx): Promise { const items = await processItems(list); return { - title: `動画カテゴリー: ${feed.title?.split('-')[0]} - chikubi.jp`, + title: `動画カテゴリー: ${feed.title?.split('-', 1)[0]} - chikubi.jp`, link: `${baseUrl}${url}`, item: items, }; diff --git a/lib/routes/chikubi/nipple-video-maker.ts b/lib/routes/chikubi/nipple-video-maker.ts index 2d2d1a2ee686..a36def0c86ef 100644 --- a/lib/routes/chikubi/nipple-video-maker.ts +++ b/lib/routes/chikubi/nipple-video-maker.ts @@ -44,7 +44,7 @@ async function handler(ctx): Promise { const items = await processItems(list); return { - title: `AVメーカー: ${feed.title?.split('-')[0]} - chikubi.jp`, + title: `AVメーカー: ${feed.title?.split('-', 1)[0]} - chikubi.jp`, link: `${baseUrl}${url}`, item: items, }; diff --git a/lib/routes/china/finance/finance.ts b/lib/routes/china/finance/finance.ts index 014043c5648b..ac174799e623 100644 --- a/lib/routes/china/finance/finance.ts +++ b/lib/routes/china/finance/finance.ts @@ -56,7 +56,7 @@ async function handler(ctx) { const $ = load(data); const categoryTitle = $('.list-hd strong').text(); const listCategory = `中华网-财经-${categoryTitle}新闻`; - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const detailsUrls = $('.item-con-inner') .toArray() .map((item) => { diff --git a/lib/routes/chinacdc/index.ts b/lib/routes/chinacdc/index.ts index 5bf1a2d60d5d..1b81fe9b7bf3 100644 --- a/lib/routes/chinacdc/index.ts +++ b/lib/routes/chinacdc/index.ts @@ -13,7 +13,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { category = 'zxyw' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '11', 10); + const limit = Number(ctx.req.query('limit') ?? '11'); const rootUrl = 'https://www.chinacdc.cn'; const targetUrl: string = new URL(category.endsWith('/') ? category : `${category}/`, rootUrl).href; diff --git a/lib/routes/chinadaily/language.ts b/lib/routes/chinadaily/language.ts index dab02950b8a0..21189db52824 100644 --- a/lib/routes/chinadaily/language.ts +++ b/lib/routes/chinadaily/language.ts @@ -14,7 +14,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { category = 'thelatest' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://language.chinadaily.com.cn'; const targetUrl: string = new URL(category, baseUrl).href; diff --git a/lib/routes/chinadegrees/province.tsx b/lib/routes/chinadegrees/province.tsx index e32c38d7da5a..e129324fe2f2 100644 --- a/lib/routes/chinadegrees/province.tsx +++ b/lib/routes/chinadegrees/province.tsx @@ -82,11 +82,11 @@ async function handler(ctx) { const data = await cache.tryGet( url, async () => { - const browser = await playwright(); - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + const context = await playwright(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' ? route.continue() : route.abort(); }); await page.goto(url, { waitUntil: 'domcontentloaded', @@ -94,7 +94,7 @@ async function handler(ctx) { await page.waitForSelector('.datalist'); const html = await page.evaluate(() => document.documentElement.innerHTML); - await browser.close(); + await context.close(); const $ = load(html); return { diff --git a/lib/routes/chinaisa/index.ts b/lib/routes/chinaisa/index.ts index 1323ceaaaba3..e9e44184a9d5 100644 --- a/lib/routes/chinaisa/index.ts +++ b/lib/routes/chinaisa/index.ts @@ -164,7 +164,7 @@ export const route: Route = { async function handler(ctx) { const { id = '58af05dfb6b4300151760176d2aad0a04c275aaadbb1315039263f021f920dcd' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const rootUrl = 'https://www.chinaisa.org.cn'; @@ -221,7 +221,7 @@ async function handler(ctx) { item.title = content('div.article_title').contents().first().text() || item.title; item.description = content('div.article_main').html(); - item.author = matches[1].split(/&/)[0]; + item.author = matches[1].split(/&/, 1)[0]; item.guid = `chinaisa-${item.guid}`; item.pubDate = parseDate(matches[2]); diff --git a/lib/routes/chinamoney/notice.ts b/lib/routes/chinamoney/notice.ts index df8c23369b11..686f1910a35a 100644 --- a/lib/routes/chinamoney/notice.ts +++ b/lib/routes/chinamoney/notice.ts @@ -109,7 +109,7 @@ async function handler(ctx) { ); return { - title: `${channels[channelId] ? channels[channelId].title + ' - ' : ''}中国货币网`, + title: `${Object.hasOwn(channels, channelId) ? channels[channelId].title + ' - ' : ''}中国货币网`, link: `${baseUrl}${channels[channelId]?.urlPath ?? ''}`, item: items, }; diff --git a/lib/routes/chinanews/index.ts b/lib/routes/chinanews/index.ts index f8b1a3b833be..2cc0f9195243 100644 --- a/lib/routes/chinanews/index.ts +++ b/lib/routes/chinanews/index.ts @@ -29,13 +29,14 @@ async function handler(ctx) { url: currentUrl, }); const $ = load(response.data); + const limit = ctx.req.query('limit'); const list = $('a', '.dd_bt') .toArray() .map((item) => ({ link: rootUrl + $(item).attr('href'), title: $(item).text(), })) - .slice(0, ctx.req.query('limit') ? Math.min(Number.parseInt(ctx.req.query('limit')), 125) : 50); + .slice(0, limit ? Number.parseInt(limit) : 50); const items = await Promise.all( list.map((item) => diff --git a/lib/routes/chinania/index.ts b/lib/routes/chinania/index.ts index 89a735d74f8f..5f0d51c7869b 100644 --- a/lib/routes/chinania/index.ts +++ b/lib/routes/chinania/index.ts @@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx) => { const { category = 'xiehuidongtai/xiehuitongzhi' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 25; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 25; const rootUrl = 'https://www.chinania.org.cn'; const currentUrl = new URL(`html/${category.endsWith('/') ? category : `${category}/`}`, rootUrl).href; @@ -62,7 +62,7 @@ export const handler = async (ctx) => { return { title, - description: title.split(/-/)[0]?.trim(), + description: title.split(/-/, 1)[0]?.trim(), link: currentUrl, item: items, allowEmpty: true, diff --git a/lib/routes/chinaratings/credit-research.ts b/lib/routes/chinaratings/credit-research.ts index 7baab932be09..fae25399a999 100644 --- a/lib/routes/chinaratings/credit-research.ts +++ b/lib/routes/chinaratings/credit-research.ts @@ -11,7 +11,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const { category = 'Industry/Comment' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '15', 10); + const limit = Number(ctx.req.query('limit') ?? '15'); const baseUrl = 'https://www.chinaratings.com.cn'; const targetUrl: string = new URL(`CreditResearch/${category.endsWith('/') ? category : `${category}/`}`, baseUrl).href; @@ -59,7 +59,7 @@ export const handler = async (ctx: Context): Promise => { const metaStr: string = $$('div.newshead p span, div.title p span').text(); const pubDateStr: string | undefined = metaStr?.match(/(\d{4}-\d{2}-\d{2})/)?.[1]; - const authors: DataItem['author'] = metaStr?.match(/来源:(.*?)/)?.[1]; + const authors: DataItem['author'] = metaStr?.match(/来源:(.*)/)?.[1]; const upDatedStr: string | undefined = pubDateStr; let processedItem: DataItem = { diff --git a/lib/routes/chinathinktanks/viewpoint.ts b/lib/routes/chinathinktanks/viewpoint.ts index 021c8654ef90..98115a0fe34c 100644 --- a/lib/routes/chinathinktanks/viewpoint.ts +++ b/lib/routes/chinathinktanks/viewpoint.ts @@ -113,7 +113,7 @@ async function handler(ctx) { ); return { - title: `中国智库网 —— ${$('title').text().split('_中国智库网')[0]}`, + title: `中国智库网 —— ${$('title').text().split('_中国智库网', 1)[0]}`, link, item: items, }; diff --git a/lib/routes/chinatimes/index.ts b/lib/routes/chinatimes/index.ts index bfeb05a87d93..db4305191c6a 100644 --- a/lib/routes/chinatimes/index.ts +++ b/lib/routes/chinatimes/index.ts @@ -43,11 +43,11 @@ async function handler(ctx) { const response = await ofetch(link); const $ = load(response); - const browser = await playwright(); + const context = await playwright(); const list = $('.articlebox-compact') .toArray() - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20) + .slice(0, ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20) .map((item) => { const $item = $(item); const a = $item.find('.title a'); @@ -66,10 +66,10 @@ async function handler(ctx) { const items = await Promise.all( list.map((item) => cache.tryGet(item.link, async () => { - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' ? request.continue() : request.abort(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' ? route.continue() : route.abort(); }); logger.http(`Requesting ${item.link}`); await page.goto(item.link, { @@ -98,7 +98,7 @@ async function handler(ctx) { ) ); - await browser.close(); + await context.close(); return { title: $('head title').text(), diff --git a/lib/routes/chinaventure/index.ts b/lib/routes/chinaventure/index.ts index aff83521686f..af8064d73bb1 100644 --- a/lib/routes/chinaventure/index.ts +++ b/lib/routes/chinaventure/index.ts @@ -57,13 +57,14 @@ async function handler(ctx) { url: currentUrl, }); const $ = load(response.data); + const limit = ctx.req.query('limit'); const list = $('a', '.common_newslist_pc') .filter((element) => $(element).attr('href')) .toArray() .map((item) => ({ link: rootUrl + $(item).attr('href'), })) - .slice(0, ctx.req.query('limit') ? Math.min(Number.parseInt(ctx.req.query('limit')), 20) : 20); + .slice(0, limit ? Number.parseInt(limit) : 20); const items = await Promise.all( list.map((item) => diff --git a/lib/routes/chinawriter/index.ts b/lib/routes/chinawriter/index.ts index 946c8bc79fb1..89a6da096b30 100644 --- a/lib/routes/chinawriter/index.ts +++ b/lib/routes/chinawriter/index.ts @@ -15,7 +15,7 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 40; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 40; const rootUrl = 'http://www.chinawriter.com.cn'; const currentUrl = `${new URL(id ?? '', rootUrl).href}/`; diff --git a/lib/routes/chnmus/exhibition.tsx b/lib/routes/chnmus/exhibition.tsx new file mode 100644 index 000000000000..8fae9bb87598 --- /dev/null +++ b/lib/routes/chnmus/exhibition.tsx @@ -0,0 +1,210 @@ +import { load } from 'cheerio'; +import { renderToString } from 'hono/jsx/dom/server'; + +import type { DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +import { namespace } from './namespace'; + +// format the date to YYYY-MM-DD and handle missing year or month +const extractDates = (durationStr: string) => { + let startDate: string | undefined; + let endDate: string | undefined; + + if (!durationStr) { + return { startDate, endDate }; + } + + const parts = durationStr.split(/——|[-—~]/).map((p) => p.trim()); // currently ——and- is used, add — or ~ for redundency + const startStr = parts[0]; + const endStr = parts[1]; + + let startYear: string | undefined; + let startMonth: string | undefined; + + const startRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/; + const startMatch = startStr.match(startRegex); + + if (startMatch) { + startYear = startMatch[1]; + startMonth = startMatch[2].padStart(2, '0'); + const startDay = startMatch[3].padStart(2, '0'); + startDate = `${startYear}-${startMonth}-${startDay}`; + } + + if (endStr && startDate) { + const endRegex = /(?:(\d{4})年)?(?:(\d{1,2})月)?(\d{1,2})日/; + const endMatch = endStr.match(endRegex); + + if (endMatch) { + const matchYear = endMatch[1]; + const matchMonth = endMatch[2]?.padStart(2, '0'); + const matchDay = endMatch[3].padStart(2, '0'); + + const finalEndYear = matchYear || startYear; + const finalEndMonth = matchMonth || startMonth; + const finalEndDay = matchDay; + endDate = `${finalEndYear}-${finalEndMonth}-${finalEndDay}`; + } + } + + return { startDate, endDate }; +}; + +export const route: Route = { + path: '/information/exhibition/:type?', + categories: ['travel'], + example: '/chnmus/information/exhibition/special', + parameters: { + type: 'Exhibition type, supported values: special(特展详情). Default: All.', + }, + name: 'Special Exhibitions', + maintainers: ['magazian'], + radar: [ + { + source: ['www.chnmus.net/ch/information/exhibition/index.html'], + target: '/information/exhibition', + }, + ], + + handler: async (ctx) => { + const type = ctx.req.param('type'); + const isSpecial = type === 'special'; + + const baseUrl = 'https://www.chnmus.net'; + const apiUrl = `${baseUrl}/ch/information/exhibition/index.html`; + const museumName = namespace.zh?.name || namespace.name; + + const response = await got({ + method: 'get', + url: apiUrl, + }); + + const $ = load(response.data); + + const list = $('.col-md-6.d-flex.fadeInBottom') + .toArray() + .map((item) => { + const $item = $(item); + const link = $item.attr('href'); + const imgUrlRaw = $item.find('.lazyload').attr('data-bg'); + const listTitle = $item.find('.common-component-box-title').text(); + + return { + title: listTitle, + itemLink: `https:${link}`, + imgUrl: `https:${imgUrlRaw}`, + }; + }); + + const items = await Promise.all( + list.map((item) => { + // use seperate cache key for special path + const cacheKey = isSpecial ? `${item.itemLink}-special` : item.itemLink; + + return cache.tryGet(cacheKey, async (): Promise> => { + const detailResponse = await got({ + method: 'get', + url: item.itemLink, + }); + const content = load(detailResponse.data); + + const pubDateRaw = content('.common-component-content-attribute-item') + .toArray() + .map((el) => content(el).text()) + .find((text) => text.includes('发布日期'))! + .replaceAll('发布日期:', '') + .trim(); + const pubDate = parseDate(pubDateRaw); + + // Default path: return as news, no detail information for return + if (!isSpecial) { + return { + title: item.title, + link: item.itemLink, + pubDate, + description: renderToString( +
+ +
+ ), + } as Record; + } + + // Special path to return detail exhibition information + const texts = content('.common-component-content-text p') + .toArray() + .map((el) => content(el).text()); + + const title = texts.find((text) => text.includes('展览名称:'))?.replaceAll('展览名称:', ''); + + // filter out items without title, for example: https://www.chnmus.net/ch/information/exhibition/details.html?id=7400076083917230080#list + if (!title) { + return {} as Record; + } + + const location = texts + .find((text) => text.includes('展览地点:'))! + .replaceAll('展览地点:', '') + .trim(); + + const fullDuration = texts + .find((text) => text.includes('展览时间:') || text.includes('开展时间:')) + ?.replaceAll(/(展览|开展)时间:/g, '') + ?.trim(); + + const { startDate, endDate } = extractDates(fullDuration || ''); + const { imgUrl, itemLink } = item; + + const description = renderToString( +
+ +
+

+ 地点: + {location} +

+

+ 开展: + {startDate ?? '未定/常设'} +

+

+ 闭展: + {endDate ?? '未定/常设'} +

+ {fullDuration && ( +

+ 原始展期:{fullDuration} +

+ )} +
+ ); + + return { + title, + link: itemLink, + pubDate, + description, + _extra: { + museumName, + title, + location, + startDate, + endDate, + itemLink, + }, + } as Record; + }) as Promise; + }) + ); + + return { + title: `${museumName} - 展览资讯${isSpecial ? ' - 特展详情' : ''}`, + link: apiUrl, + language: 'zh-CN', + item: items.filter((item) => item.title) as DataItem[], + }; + }, +}; diff --git a/lib/routes/chnmus/namespace.ts b/lib/routes/chnmus/namespace.ts new file mode 100644 index 000000000000..e5fe10c6228f --- /dev/null +++ b/lib/routes/chnmus/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Henan Museum', + url: 'www.chnmus.net', + + zh: { + name: '河南博物院', + }, +}; diff --git a/lib/routes/chnmuseum/xingnew.ts b/lib/routes/chnmuseum/xingnew.ts index afccbdacb0ab..cc234a046ce6 100644 --- a/lib/routes/chnmuseum/xingnew.ts +++ b/lib/routes/chnmuseum/xingnew.ts @@ -9,7 +9,7 @@ import timezone from '@/utils/timezone'; export const route: Route = { path: '/zx/xingnew', categories: ['travel'], - example: '/zx/xingnew', + example: '/chnmuseum/zx/xingnew', parameters: {}, features: { requireConfig: false, diff --git a/lib/routes/chnmuseum/xwzt.ts b/lib/routes/chnmuseum/xwzt.ts index 719d91e62cfb..1953957b3cd6 100644 --- a/lib/routes/chnmuseum/xwzt.ts +++ b/lib/routes/chnmuseum/xwzt.ts @@ -6,7 +6,7 @@ import ofetch from '@/utils/ofetch'; export const route: Route = { path: '/zx/xwzt', categories: ['travel'], - example: '/zx/xwzt', + example: '/chnmuseum/zx/xwzt', parameters: {}, features: { requireConfig: false, diff --git a/lib/routes/chnmuseum/zl.tsx b/lib/routes/chnmuseum/zl.tsx index 43f86b39e31c..71c0b0d329a6 100644 --- a/lib/routes/chnmuseum/zl.tsx +++ b/lib/routes/chnmuseum/zl.tsx @@ -87,12 +87,12 @@ const parseExhibitionDuration = (duration: string) => { }; // to identify the route config and titletag based on type and subtype, this function is used in both route handler and radar to ensure consistency -const resolveRouteConfig = (type: string | undefined, subType: string | undefined, baseUrl: string) => { +const resolveRouteConfig = (type: string | undefined, subtype: string | undefined, baseUrl: string) => { let url = `${baseUrl}/zl/`; let cleanType = ''; if (type) { - cleanType = subType ? `${type}/${subType}` : type; + cleanType = subtype ? `${type}/${subtype}` : type; url = `${baseUrl}/zl/${cleanType}/`; } return { @@ -103,7 +103,7 @@ const resolveRouteConfig = (type: string | undefined, subType: string | undefine }; // to concurrent or single-page retrieval according to titletag -const fetchTargetElements = async (cleanType: string, subType: string | undefined, url: string, baseUrl: string) => { +const fetchTargetElements = async (cleanType: string, subtype: string | undefined, url: string, baseUrl: string) => { const items: Array<{ $item: any; contextUrl: string }> = []; // Use a Set to track visited links and filter out duplicate HTML elements directly at the source // (e.g., when the same exhibition appears in both the main list and a specific sub-category list). @@ -169,12 +169,12 @@ export const route: Route = { ], handler: async (ctx: Context): Promise => { const type = ctx.req.param('type')?.trim(); - const subType = ctx.req.param('subType')?.trim(); + const subtype = ctx.req.param('subType')?.trim(); const museumName = namespace.zh?.name || namespace.name; const baseUrl = 'https://www.chnmuseum.cn'; - const { cleanType, url, titleTag } = resolveRouteConfig(type, subType, baseUrl); - const itemsToParse = await fetchTargetElements(cleanType, subType, url, baseUrl); + const { cleanType, url, titleTag } = resolveRouteConfig(type, subtype, baseUrl); + const itemsToParse = await fetchTargetElements(cleanType, subtype, url, baseUrl); const list = ( await Promise.all( diff --git a/lib/routes/chuanliu/nice.tsx b/lib/routes/chuanliu/nice.tsx index ecd758138885..cd609e4bb187 100644 --- a/lib/routes/chuanliu/nice.tsx +++ b/lib/routes/chuanliu/nice.tsx @@ -36,7 +36,7 @@ export const route: Route = { }; async function handler(ctx) { - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 100; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 100; const rootUrl = 'https://chuanliu.org'; const apiRootUrl = 'https://s.chuanliu.org'; diff --git a/lib/routes/cib/whpj.ts b/lib/routes/cib/whpj.ts index ec3637e60655..4f5335070f13 100644 --- a/lib/routes/cib/whpj.ts +++ b/lib/routes/cib/whpj.ts @@ -44,11 +44,11 @@ async function handler(ctx) { }); const response = await got('https://personalbank.cib.com.cn/pers/main/pubinfo/ifxQuotationQuery.do', { agent: { https: agent } }); - const cookies = response.headers['set-cookie'].map((item) => item.split(';')[0]).join(';'); + const cookies = response.headers['set-cookie'].map((item) => item.split(';', 1)[0]).join(';'); const $ = load(response.data); let date = $('div.main-body').find('div.labe_text').text(); - date = date.split('\n\t')[1].replace('日期:', '').trim(); + date = date.split('\n\t', 2)[1].replace('日期:', '').trim(); date = date.slice(0, 11) + date.slice(15); const link = 'https://personalbank.cib.com.cn/pers/main/pubinfo/ifxQuotationQuery/list?_search=false&dataSet.rows=80&dataSet.page=1&dataSet.sidx=&dataSet.sord=asc'; diff --git a/lib/routes/cih-index/report.ts b/lib/routes/cih-index/report.ts index b3f6a0263a49..a61197bdbdfe 100644 --- a/lib/routes/cih-index/report.ts +++ b/lib/routes/cih-index/report.ts @@ -40,7 +40,7 @@ async function handler(ctx) { const initialState = JSON.parse( $('script:contains("window.__INITIAL_STATE__")') .text() - .match(/window\.__INITIAL_STATE__\s*=\s*({.*?});/)?.[1] || '{}' + .match(/window\.__INITIAL_STATE__\s*=\s*(\{.*?\});/)?.[1] || '{}' ); const { dataResult, indNavLists, secondNameFilter, tagList, param } = initialState.data; diff --git a/lib/routes/cisia/index.ts b/lib/routes/cisia/index.ts index c1e5832b935f..0c5492072d18 100644 --- a/lib/routes/cisia/index.ts +++ b/lib/routes/cisia/index.ts @@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx) => { const { id = '9' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const domain = 'www.cisia.org'; const rootUrl = `http://${domain}`; @@ -35,7 +35,7 @@ export const handler = async (ctx) => { items = await Promise.all( items.map((item) => cache.tryGet(item.link, async () => { - if (!/^https?:\/\/www\.cisia\.org(\/[^\s]*)?$/.test(item.link)) { + if (!/^https?:\/\/www\.cisia\.org(?:\/\S*)?$/.test(item.link)) { return item; } diff --git a/lib/routes/civitai/models.ts b/lib/routes/civitai/models.ts index d1a75ada0eea..b6735c193fc5 100644 --- a/lib/routes/civitai/models.ts +++ b/lib/routes/civitai/models.ts @@ -38,7 +38,7 @@ async function handler() { const items = data.items.map((item) => ({ title: item.name, link: `https://civitai.com/models/${item.id}`, - description: `${item.modelVersions?.[0]?.images?.map((image) => ``).join('\n')}${item.description}`, + description: `${item.modelVersions?.[0]?.images?.map((image) => ``).join('\n')}${item.description}`, pubDate: parseDate(item.lastVersionAt), author: item.creator?.username, category: item.tags, diff --git a/lib/routes/cjlu/yjsy/index.ts b/lib/routes/cjlu/yjsy/index.ts index 68804d6e1930..b91cd90ff87c 100644 --- a/lib/routes/cjlu/yjsy/index.ts +++ b/lib/routes/cjlu/yjsy/index.ts @@ -83,22 +83,21 @@ export const route: Route = { async function handler(ctx) { const cate = ctx.req.param('cate'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10; const url = `${host}index/${cate}.htm`; - const { page, destroy, browser } = await getPlaywrightPage(url, { + const { page, destroy } = await getPlaywrightPage(url, { onBeforeLoad: async (page) => { await page.setExtraHTTPHeaders(headers); - await page.setUserAgent(headers['User-Agent']); - await page.setRequestInterception(true); - page.on('request', (request) => { - allowedResourceTypes.has(request.resourceType()) ? request.continue() : request.abort(); + await page.route('**/*', (route) => { + const request = route.request(); + allowedResourceTypes.has(request.resourceType()) ? route.continue() : route.abort(); }); }, - gotoConfig: { waitUntil: 'networkidle2' }, + gotoConfig: { waitUntil: 'networkidle' }, }); - const cookies = await browser.cookies(); + const cookies = await page.context().cookies(); const cookieString = cookies.map((c) => `${c.name}=${c.value}`).join('; '); const response = await page.content(); diff --git a/lib/routes/claude/blog.ts b/lib/routes/claude/blog.ts index 9cc243c6baea..50b2828eed45 100644 --- a/lib/routes/claude/blog.ts +++ b/lib/routes/claude/blog.ts @@ -37,7 +37,7 @@ async function handler(ctx) { const link = `${baseUrl}/blog`; const response = await ofetch(link); const $ = load(response); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const list: DataItem[] = $('.blog_cms_list article.card_blog_list_wrap') .toArray() diff --git a/lib/routes/claude/code-changelog.ts b/lib/routes/claude/code-changelog.ts index 7d8483231530..167f71e7212a 100644 --- a/lib/routes/claude/code-changelog.ts +++ b/lib/routes/claude/code-changelog.ts @@ -7,7 +7,7 @@ import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; const handler = async (ctx: Context): Promise => { - const limit = Number.parseInt(ctx.req.query('limit') ?? '25', 10); + const limit = Number(ctx.req.query('limit') ?? '25'); const baseUrl = 'https://code.claude.com'; const targetUrl = `${baseUrl}/docs/en/changelog`; diff --git a/lib/routes/cline/blog.ts b/lib/routes/cline/blog.ts index 7455759c8df2..4ae95ce50cea 100644 --- a/lib/routes/cline/blog.ts +++ b/lib/routes/cline/blog.ts @@ -21,7 +21,7 @@ function extractArticlesFromDOM($: CheerioAPI): DataItem[] { // Extract date and author with single regex const metaText = element.find('.text-sm.text-slate-500').text().trim(); - const metaMatch = metaText.match(/^([^•]+)\s*•\s*([A-Za-z]+\s+\d{1,2},?\s+\d{4})/); + const metaMatch = metaText.match(/^([^•]+)•\s*([A-Z]+\s+\d{1,2},?\s+\d{4})/i); const author = metaMatch ? metaMatch[1].trim() : 'Cline Team'; const pubDate = metaMatch ? parseDate(metaMatch[2]) : undefined; diff --git a/lib/routes/cloudflarestatus/index.tsx b/lib/routes/cloudflarestatus/index.tsx index 12d2ff4c228e..74444264f585 100644 --- a/lib/routes/cloudflarestatus/index.tsx +++ b/lib/routes/cloudflarestatus/index.tsx @@ -11,7 +11,7 @@ import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '100', 10); + const limit = Number(ctx.req.query('limit') ?? '100'); const baseUrl = 'https://www.cloudflarestatus.com'; @@ -40,7 +40,7 @@ export const handler = async (ctx: Context): Promise => { ); const pubDateStr: string | undefined = $el.find('span.ago').attr('data-datetime-unix'); - const linkUrl: string | undefined = $actualTitleEl.attr('href') ? new URL($actualTitleEl.attr('href') as string, baseUrl).toString() : undefined; + const linkUrl: string | undefined = $actualTitleEl.attr('href') ? new URL($actualTitleEl.attr('href') as string, baseUrl).href : undefined; const categories: string[] = [type].filter(Boolean); const guid: string = linkUrl ? `${linkUrl}#${pubDateStr}` : ''; const upDatedStr: string | undefined = pubDateStr; diff --git a/lib/routes/cls/subject.ts b/lib/routes/cls/subject.ts index 9b6e07d42f1f..9216bdfe4fc0 100644 --- a/lib/routes/cls/subject.ts +++ b/lib/routes/cls/subject.ts @@ -10,7 +10,7 @@ import { getSearchParams, rootUrl } from './utils'; export const handler = async (ctx) => { const { id = '1103' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const currentUrl = new URL(`subject/${id}`, rootUrl).href; const apiUrl = new URL(`api/subject/${id}/article`, rootUrl).href; diff --git a/lib/routes/cls/utils.ts b/lib/routes/cls/utils.ts index 4dd78a58d9bb..701fedcca649 100644 --- a/lib/routes/cls/utils.ts +++ b/lib/routes/cls/utils.ts @@ -12,7 +12,8 @@ const getSearchParams = (moreParams?) => { const filtered = Object.fromEntries(Object.entries({ ...params, ...moreParams }).filter(([_, v]) => v !== undefined)); const searchParams = new URLSearchParams(filtered as Record); searchParams.sort(); - searchParams.append('sign', CryptoJS.MD5(CryptoJS.SHA1(searchParams.toString()).toString()).toString()); + const sha1 = CryptoJS.SHA1(searchParams.toString()).toString(); + searchParams.append('sign', CryptoJS.MD5(sha1).toString()); return Object.fromEntries(searchParams); }; diff --git a/lib/routes/cmde/index.ts b/lib/routes/cmde/index.ts index 0f7776e45237..6e3d8c1cfcb7 100644 --- a/lib/routes/cmde/index.ts +++ b/lib/routes/cmde/index.ts @@ -18,12 +18,12 @@ export const route: Route = { async function handler(ctx) { const cate = ctx.req.param('cate') ?? 'xwdt/zxyw'; const url = `${rootURL}/${cate}/`; - const browser = await playwright(); + const context = await playwright(); const data = await cache.tryGet(url, async () => { - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' ? route.continue() : route.abort(); }); await page.goto(url, { waitUntil: 'domcontentloaded', @@ -52,10 +52,10 @@ async function handler(ctx) { const items = await Promise.all( data.items.map((item) => cache.tryGet(item.link, async () => { - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' ? route.continue() : route.abort(); }); await page.goto(item.link, { waitUntil: 'domcontentloaded', @@ -72,7 +72,7 @@ async function handler(ctx) { ) ); - await browser.close(); + await context.close(); return { title: data.title, diff --git a/lib/routes/cmpxchg8b/articles.ts b/lib/routes/cmpxchg8b/articles.ts index 63a883b2a363..55d1fac15dc5 100644 --- a/lib/routes/cmpxchg8b/articles.ts +++ b/lib/routes/cmpxchg8b/articles.ts @@ -42,7 +42,7 @@ async function handler() { item = $(item); return { title: item.find('li').text(), - link: new URL(item.find('li a').attr('href'), baseUrl).toString(), + link: new URL(item.find('li a').attr('href'), baseUrl).href, author, }; }); @@ -64,7 +64,7 @@ async function handler() { return { title, - link: new URL('#articles', baseUrl).toString(), + link: new URL('#articles', baseUrl).href, item: items, }; } diff --git a/lib/routes/cna/index.ts b/lib/routes/cna/index.ts index 65f2b41f5845..77006e48399c 100644 --- a/lib/routes/cna/index.ts +++ b/lib/routes/cna/index.ts @@ -30,7 +30,7 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id') || 'aall'; const isTopic = /^\d+$/.test(id); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; // The API used by the website when hitting "看更多內容" const { data: response } = await got({ diff --git a/lib/routes/cnbeta/category.ts b/lib/routes/cnbeta/category.ts index f2a916204809..d7f53286cce2 100644 --- a/lib/routes/cnbeta/category.ts +++ b/lib/routes/cnbeta/category.ts @@ -4,7 +4,7 @@ import { handler } from './common'; export const route: Route = { name: '分类', - path: ['/category/:id'], + path: '/category/:id', example: '/cnbeta/category/movie', maintainers: ['nczitzk'], parameters: { diff --git a/lib/routes/cnbeta/common.ts b/lib/routes/cnbeta/common.ts index a96f3a49b5c8..60f2e61236bc 100644 --- a/lib/routes/cnbeta/common.ts +++ b/lib/routes/cnbeta/common.ts @@ -25,7 +25,7 @@ export async function handler(ctx) { ? response.data.result.list.map((item) => ({ title: item.title, description: item.hometext, - author: item.source.split('@http')[0], + author: item.source.split('@http', 1)[0], pubDate: timezone(parseDate(item.inputtime), +8), link: item.url_show.startsWith('//') ? `https:${item.url_show}` : item.url_show.replace('http:', 'https:'), category: item.label.name, diff --git a/lib/routes/cnbeta/index.ts b/lib/routes/cnbeta/index.ts index 2d1d8d94f0ed..1900b9e2e09a 100644 --- a/lib/routes/cnbeta/index.ts +++ b/lib/routes/cnbeta/index.ts @@ -4,7 +4,7 @@ import { handler } from './common'; export const route: Route = { name: '头条资讯', - path: ['/'], + path: '/', example: '/cnbeta', radar: [ { diff --git a/lib/routes/cnbeta/topics.ts b/lib/routes/cnbeta/topics.ts index a2d986130d02..5aa977c03470 100644 --- a/lib/routes/cnbeta/topics.ts +++ b/lib/routes/cnbeta/topics.ts @@ -4,7 +4,7 @@ import { handler } from './common'; export const route: Route = { name: '主题', - path: ['/topics/:id'], + path: '/topics/:id', example: '/cnbeta/topics/453', maintainers: ['cczhong11', 'nczitzk'], parameters: { diff --git a/lib/routes/cncf/index.ts b/lib/routes/cncf/index.ts index a4d07888f806..b3e48a90a757 100644 --- a/lib/routes/cncf/index.ts +++ b/lib/routes/cncf/index.ts @@ -40,7 +40,7 @@ async function handler(ctx) { .map((item) => ({ title: $(item).find('span.post-archive__title').text().trim(), link: $(item).find('span.post-archive__title > a').attr('href'), - pubDate: parseDate($(item).find('span.post-archive__item_date').text().split('|')[0]), + pubDate: parseDate($(item).find('span.post-archive__item_date').text().split('|', 1)[0]), })); const items = await Promise.all( diff --git a/lib/routes/cngold/index.ts b/lib/routes/cngold/index.ts index 400c25e2d611..f9d546a39b5f 100644 --- a/lib/routes/cngold/index.ts +++ b/lib/routes/cngold/index.ts @@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx) => { const { category = 'news-325' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 12; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 12; const rootUrl = 'https://www.cngold.org.cn'; const currentUrl = new URL(`${category}.html`, rootUrl).href; diff --git a/lib/routes/cnki/debut.ts b/lib/routes/cnki/debut.ts index a269a73fdbf3..d855b28a7d72 100644 --- a/lib/routes/cnki/debut.ts +++ b/lib/routes/cnki/debut.ts @@ -36,7 +36,8 @@ async function handler(ctx) { const name = ctx.req.param('name'); const journalUrl = `${rootUrl}/knavi/JournalDetail?pcode=CjFD&pykm=${name}`; - const title = await got.get(journalUrl).then((res) => load(res.data)('head > title').text()); + const res = await got.get(journalUrl); + const title = load(res.data)('head > title').text(); const outlineUrl = `${rootUrl}/knavi/JournalDetail/GetnfAllOutline`; const response = await got({ diff --git a/lib/routes/cnki/journals.ts b/lib/routes/cnki/journals.ts index 24cd3b80d73c..ec101d614eef 100644 --- a/lib/routes/cnki/journals.ts +++ b/lib/routes/cnki/journals.ts @@ -64,16 +64,15 @@ async function handler(ctx) { } const journalUrl = `${rootUrl}/knavi/journals/${name}/detail`; - const title = await got.get(journalUrl).then((res) => load(res.data)('head > title').text()); + const titleRes = await got.get(journalUrl); + const title = load(titleRes.data)('head > title').text(); const yearListUrl = `${rootUrl}/knavi/journals/${name}/yearList?pIdx=0`; - const { code, date } = await got.get(yearListUrl).then((res) => { - const $ = load(res.data); - const code = $('.yearissuepage').find('dl').first().find('dd').find('a').first().attr('value'); - const date = parseDate($('.yearissuepage').find('dl').first().find('dd').find('a').first().attr('id').replace('yq', ''), 'YYYYMM'); - return { code, date }; - }); + const yearListRes = await got.get(yearListUrl); + const $yearList = load(yearListRes.data); + const code = $yearList('.yearissuepage').find('dl').first().find('dd').find('a').first().attr('value'); + const date = parseDate($yearList('.yearissuepage').find('dl').first().find('dd').find('a').first().attr('id').replace('yq', ''), 'YYYYMM'); const yearIssueUrl = `${rootUrl}/knavi/journals/${name}/papers?yearIssue=${code}&pageIdx=0&pcode=CJFD,CCJD`; const response = await got.post(yearIssueUrl); diff --git a/lib/routes/cnljxh/index.ts b/lib/routes/cnljxh/index.ts index a78a42d0df14..0af711fc0cc2 100644 --- a/lib/routes/cnljxh/index.ts +++ b/lib/routes/cnljxh/index.ts @@ -11,7 +11,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const { category = 'news', id = '10' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '20', 10); + const limit = Number(ctx.req.query('limit') ?? '20'); const baseUrl = 'https://www.cnljxh.org.cn'; const targetUrl: string = new URL(`${category}/?classid=${id}`, baseUrl).href; @@ -55,7 +55,7 @@ export const handler = async (ctx: Context): Promise => { const title: string = $$('div.content_title h2').text(); const description: string | undefined = $$('div.content_div').html() ?? ''; - const authors: DataItem['author'] = $$('div.content_title p').text().split(/\s/)[0]?.split(/:/).pop(); + const authors: DataItem['author'] = $$('div.content_title p').text().split(/\s/, 1)[0]?.split(/:/).pop(); let processedItem: DataItem = { title, diff --git a/lib/routes/cockroachlabs/blog.ts b/lib/routes/cockroachlabs/blog.ts index a5ef5cc2394a..f23a108eea52 100644 --- a/lib/routes/cockroachlabs/blog.ts +++ b/lib/routes/cockroachlabs/blog.ts @@ -88,7 +88,7 @@ async function handler(ctx) { try { const date = new Date(dateText); if (!Number.isNaN(date.getTime())) { - pubDate = parseDate(date.toISOString().split('T')[0]); + pubDate = parseDate(date.toISOString().split('T', 1)[0]); } } catch { // Ignore parsing errors diff --git a/lib/routes/codefather/posts.ts b/lib/routes/codefather/posts.ts index 84412c0dbce2..9953e2dae37a 100644 --- a/lib/routes/codefather/posts.ts +++ b/lib/routes/codefather/posts.ts @@ -88,7 +88,7 @@ async function handler(ctx) { } return { - title: content.split('\n')[0] || '无标题', + title: content.split('\n', 1)[0] || '无标题', link: `https://www.codefather.cn/post/${item.id}`, description, pubDate: parseDate(item.createTime as number), diff --git a/lib/routes/cohere/index.ts b/lib/routes/cohere/index.ts index b0c52eef3d14..56cd6aa56bb0 100644 --- a/lib/routes/cohere/index.ts +++ b/lib/routes/cohere/index.ts @@ -3,7 +3,7 @@ import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: ['/blog'], + path: '/blog', name: 'Blog', url: 'cohere.com/blog', maintainers: ['Loongphy'], diff --git a/lib/routes/coindesk/utils.ts b/lib/routes/coindesk/utils.ts index a8ed6705a47b..c5124704cfcc 100644 --- a/lib/routes/coindesk/utils.ts +++ b/lib/routes/coindesk/utils.ts @@ -10,7 +10,7 @@ export const parseItem = async (item) => { $('.article-ad, #strategy-rules-player-wrapper, [data-module-name="newsletter-article-sign-up-module"], div.flex.flex-col.gap-2').remove(); const cover = $('.article-content-wrapper figure'); - cover.find('img').attr('src', cover.find('img').attr('url')?.split('?')[0]); + cover.find('img').attr('src', cover.find('img').attr('url')?.split('?', 1)[0]); cover.find('img').removeAttr('style srcset url'); item.description = @@ -21,7 +21,7 @@ export const parseItem = async (item) => { .join(''); item.pubDate = parseDate(ldJson.datePublished); item.author = ldJson.author.map((a) => ({ name: a.name })); - item.image = ldJson.image.url.split('?')[0]; + item.image = ldJson.image.url.split('?', 1)[0]; return item; }; diff --git a/lib/routes/cointelegraph/index.ts b/lib/routes/cointelegraph/index.ts index e984e6d7f64c..33bb1d2d1e71 100644 --- a/lib/routes/cointelegraph/index.ts +++ b/lib/routes/cointelegraph/index.ts @@ -41,7 +41,7 @@ async function handler(): Promise { .filter((item) => item.link && /\/news|\/explained|\/innovation-circle/.test(item.link)) .map((item) => ({ ...item, - link: item.link?.split('?')[0], + link: item.link?.split('?', 1)[0], })) .map((item) => cache.tryGet(item.link!, async () => { diff --git a/lib/routes/colamanga/manga.ts b/lib/routes/colamanga/manga.ts index 626f1b0d8067..bda4c200b721 100644 --- a/lib/routes/colamanga/manga.ts +++ b/lib/routes/colamanga/manga.ts @@ -43,14 +43,13 @@ async function handler(ctx: Context) { const id = ctx.req.param('id'); const url = `https://${domain}/${id}`; - const browser = await playwright(); + const context = await playwright(); - const page = await browser.newPage(); + const page = await context.newPage(); - await page.setRequestInterception(true); - - page.on('request', (request) => { - request.resourceType() === 'document' ? request.continue() : request.abort(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' ? route.continue() : route.abort(); }); logger.http(`Requesting ${url}`); @@ -60,7 +59,7 @@ async function handler(ctx: Context) { }); const response = await page.content(); - await browser.close(); + await context.close(); const $ = load(response); diff --git a/lib/routes/comicat/search.ts b/lib/routes/comicat/search.ts index 46d191f7aa86..6051a5916418 100644 --- a/lib/routes/comicat/search.ts +++ b/lib/routes/comicat/search.ts @@ -44,8 +44,8 @@ async function handler(ctx) { const { data: response } = await got(item.link); const $ = load(response); - item.pubDate = parseDate($('div.main > div.slayout > div > div.c1 > div:nth-child(1) > div > p:nth-child(4)').text().split('发布时间: ')[1]); - const marginLink = `magnet:?xt=urn:btih:${$('#text_hash_id').text().split(',特征码:')[1]}`.trim(); + item.pubDate = parseDate($('div.main > div.slayout > div > div.c1 > div:nth-child(1) > div > p:nth-child(4)').text().split('发布时间: ', 2)[1]); + const marginLink = `magnet:?xt=urn:btih:${$('#text_hash_id').text().split(',特征码:', 2)[1]}`.trim(); item.enclosure_url = marginLink; item.enclosure_type = 'application/x-bittorrent'; item.description = $('#btm > div.main > div.slayout > div > div.c2 > div:nth-child(1) > div.intro').html(); diff --git a/lib/routes/cool18/index.ts b/lib/routes/cool18/index.ts index f9d9da1b1453..189fe1aa4077 100644 --- a/lib/routes/cool18/index.ts +++ b/lib/routes/cool18/index.ts @@ -65,7 +65,7 @@ function buildUrl(rootUrl: string, type: PostType, keyword: string | undefined, function extractHomeList($: CheerioAPI, rootUrl: string, limit: number): DataItem[] { try { const scriptText = $('script:contains("_PageData")').text(); - const match = scriptText.match(/const\s+_PageData\s*=\s*(\[[\s\S]*?]);/); + const match = scriptText.match(/const\s+_PageData\s*=\s*(\[[\s\S]*?\]);/); if (!match?.[1]) { return []; @@ -225,7 +225,7 @@ async function handler(ctx: Context) { const $ = load(response); const limitQuery = ctx.req.query('limit'); - const limit = limitQuery ? Number.parseInt(limitQuery as string, 10) : 20; + const limit = limitQuery ? Number(limitQuery as string) : 20; let list: DataItem[]; diff --git a/lib/routes/coolapk/hot.ts b/lib/routes/coolapk/hot.ts index 6aec2286542c..2899e09eec32 100644 --- a/lib/routes/coolapk/hot.ts +++ b/lib/routes/coolapk/hot.ts @@ -5,7 +5,6 @@ import utils from './utils'; const getLinkAndTitle = (type, period) => { const baseURL = 'https://api.coolapk.com/v6/page/dataList?url='; - let link; const res = {}; const types = { jrrm: { @@ -32,6 +31,12 @@ const getLinkAndTitle = (type, period) => { }, }; + if (type === 'jrrm') { + res.link = types.jrrm.url; + res.title = types.jrrm.title; + return res; + } + let link; const periods = { daily: { description: '日榜', @@ -42,12 +47,7 @@ const getLinkAndTitle = (type, period) => { statType: '7days', }, }; - - if (type === 'jrrm') { - res.link = types.jrrm.url; - res.title = types.jrrm.title; - return res; - } else if (type === 'ktb') { + if (type === 'ktb') { const trans = { daily: { description: '周榜', diff --git a/lib/routes/coolapk/tuwen.ts b/lib/routes/coolapk/tuwen.ts index 4b5b7130c93d..927affaee486 100644 --- a/lib/routes/coolapk/tuwen.ts +++ b/lib/routes/coolapk/tuwen.ts @@ -4,7 +4,7 @@ import got from '@/utils/got'; import utils from './utils'; export const route: Route = { - path: ['/tuwen/:type?'], + path: '/tuwen/:type?', categories: ['social-media'], example: '/coolapk/tuwen', parameters: { type: '默认为hot' }, diff --git a/lib/routes/coolbuy/index.tsx b/lib/routes/coolbuy/index.tsx index 0c33ba8ad7a6..694bcff4403f 100644 --- a/lib/routes/coolbuy/index.tsx +++ b/lib/routes/coolbuy/index.tsx @@ -8,7 +8,7 @@ import { ViewType } from '@/types'; import ofetch from '@/utils/ofetch'; export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '50', 10); + const limit = Number(ctx.req.query('limit') ?? '50'); const baseUrl = 'https://coolbuy.com'; const imageBaseUrl = 'https://mcache.ifanr.cn'; @@ -29,8 +29,8 @@ export const handler = async (ctx: Context): Promise => { const items: DataItem[] = response.objects.slice(0, limit).map((item): DataItem => { const title: string = item.title; - const image: string | undefined = item.cover_image?.split(/\?/)?.[0]; - const banner: string | undefined = item.display_image?.split(/\?/)?.[0]; + const image: string | undefined = item.cover_image?.split(/\?/, 1)?.[0]; + const banner: string | undefined = item.display_image?.split(/\?/, 1)?.[0]; const images = [banner, image].filter(Boolean).map((image) => ({ src: image, diff --git a/lib/routes/coomer/index.tsx b/lib/routes/coomer/index.tsx index cdd0cd8d4c85..d591a5e53215 100644 --- a/lib/routes/coomer/index.tsx +++ b/lib/routes/coomer/index.tsx @@ -122,7 +122,7 @@ async function handler(ctx) { } enclosureInfo = { - enclosure_url: new URL(src, rootUrl).toString(), + enclosure_url: new URL(src, rootUrl).href, enclosure_type: mimeType, }; }); diff --git a/lib/routes/costar/press-releases.ts b/lib/routes/costar/press-releases.ts index 579c1afa2580..2f192d07a287 100644 --- a/lib/routes/costar/press-releases.ts +++ b/lib/routes/costar/press-releases.ts @@ -11,7 +11,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const { filter } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '10', 10); + const limit = Number(ctx.req.query('limit') ?? '10'); const baseUrl = 'https://www.costar.com'; const targetUrl: string = new URL(`products/benchmark/resources/press-releases${filter ? `?${filter}` : ''}`, baseUrl).href; diff --git a/lib/routes/counter-strike/news.ts b/lib/routes/counter-strike/news.ts index 0053926a607b..5999283224ba 100644 --- a/lib/routes/counter-strike/news.ts +++ b/lib/routes/counter-strike/news.ts @@ -53,7 +53,7 @@ const customPreset: PresetFactory = presetHTML5.extend((tags) => ({ export const handler = async (ctx) => { const { category = 'all', language = 'english' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 100; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 100; const rootUrl = 'https://www.counter-strike.net'; const apiRootUrl = 'https://store.steampowered.com'; diff --git a/lib/routes/cpcaauto/index.ts b/lib/routes/cpcaauto/index.ts index 770dcc41d580..ce17214d10b3 100644 --- a/lib/routes/cpcaauto/index.ts +++ b/lib/routes/cpcaauto/index.ts @@ -8,7 +8,7 @@ import timezone from '@/utils/timezone'; export const handler = async (ctx) => { const { type = 'news', id } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const rootUrl = 'http://cpcaauto.com'; const currentUrl = new URL(`news.php${type ? `?types=${type}${id ? `&anid=${id}` : ''}` : ''}`, rootUrl).href; diff --git a/lib/routes/creative-comic/book.tsx b/lib/routes/creative-comic/book.tsx index 8951f809c71f..37323d86354d 100644 --- a/lib/routes/creative-comic/book.tsx +++ b/lib/routes/creative-comic/book.tsx @@ -40,10 +40,12 @@ async function handler(ctx) { data: { data: chapters }, } = await getChapters(id, uuid); + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 3; + const items = await Promise.all( chapters.chapters .toSorted((a, b) => b.idx - a.idx) - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 3) + .slice(0, limit) .map(async (c) => { let pages; if (coverOnly !== 'true' && coverOnly !== '1') { diff --git a/lib/routes/cryptoslate/index.ts b/lib/routes/cryptoslate/index.ts index abebfd6e0c93..77c4725f6587 100644 --- a/lib/routes/cryptoslate/index.ts +++ b/lib/routes/cryptoslate/index.ts @@ -46,7 +46,7 @@ async function handler(ctx): Promise { try { // Clean URL by removing query parameters - const cleanUrl = item.link.split('?')[0]; + const cleanUrl = item.link.split('?', 1)[0]; return { title: item.title || 'Untitled', diff --git a/lib/routes/cs/index.ts b/lib/routes/cs/index.ts index c9fb5dccd9cd..22eba286bbb9 100644 --- a/lib/routes/cs/index.ts +++ b/lib/routes/cs/index.ts @@ -72,7 +72,7 @@ export const route: Route = { async function handler(ctx) { const { category = 'xwzx' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const rootUrl = 'https://www.cs.com.cn'; const currentUrl = new URL(category.endsWith('/') ? category : `${category}/`, rootUrl).href; diff --git a/lib/routes/cs/video.ts b/lib/routes/cs/video.ts index 9669cd574d6c..e298ca23ced0 100644 --- a/lib/routes/cs/video.ts +++ b/lib/routes/cs/video.ts @@ -27,7 +27,7 @@ export const route: Route = { async function handler(ctx) { const { category = '今日聚焦' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 50; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 50; const rootUrl = 'https://video.cs.com.cn'; const apiCategoryUrl = new URL('web/api/getCategory', rootUrl).href; diff --git a/lib/routes/ctinews/topic.ts b/lib/routes/ctinews/topic.ts index ec1d4b745feb..9560bcacbfb1 100644 --- a/lib/routes/ctinews/topic.ts +++ b/lib/routes/ctinews/topic.ts @@ -88,7 +88,7 @@ async function handler(ctx) { const $ = load(response); if (item.link?.includes('/videos/')) { const ldJson = JSON.parse($('script[type="application/ld+json"]:contains("VideoObject")').text()); - const videoId = ldJson.embedUrl.match(/embed\/([a-zA-Z0-9_-]+)/)?.[1]; + const videoId = ldJson.embedUrl.match(/embed\/([\w-]+)/)?.[1]; item.description = `
` + diff --git a/lib/routes/cts/news.ts b/lib/routes/cts/news.ts index ce35dba6525b..e7222482d8a2 100644 --- a/lib/routes/cts/news.ts +++ b/lib/routes/cts/news.ts @@ -37,8 +37,9 @@ async function handler(ctx) { const currentUrl = `https://news.cts.com.tw/${category}/index.html`; const response = await got(currentUrl); const $ = load(response.data); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20; const items = await pMap( - $('#newslist-top a[title]').slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20), + $('#newslist-top a[title]').slice(0, limit), (item) => { item = $(item); const link = item.attr('href'); diff --git a/lib/routes/cuilingmag/index.ts b/lib/routes/cuilingmag/index.ts index 47ba39a7fc08..e2e612f9503b 100644 --- a/lib/routes/cuilingmag/index.ts +++ b/lib/routes/cuilingmag/index.ts @@ -9,7 +9,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx) => { const { category } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 12; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 12; const rootUrl = 'https://www.cuilingmag.com'; const currentUrl = new URL(category ? `category/${category}` : '', rootUrl).href; diff --git a/lib/routes/cursor/blog.ts b/lib/routes/cursor/blog.ts index 11e7014b2cf3..d73e83fe0d39 100644 --- a/lib/routes/cursor/blog.ts +++ b/lib/routes/cursor/blog.ts @@ -8,7 +8,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const { topic, locale } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '10', 10); + const limit = Number(ctx.req.query('limit') ?? '10'); const baseUrl = 'https://cursor.com'; const normalizedTopic = topic === 'all' ? undefined : topic; diff --git a/lib/routes/cursor/changelog.ts b/lib/routes/cursor/changelog.ts index fc7aadbbdddf..a3965126ba24 100644 --- a/lib/routes/cursor/changelog.ts +++ b/lib/routes/cursor/changelog.ts @@ -10,7 +10,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const locale = ctx.req.param('locale'); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '100', 10); + const limit = Number(ctx.req.query('limit') ?? '100'); const baseUrl = 'https://cursor.com'; const localeSegment = locale ? `/${locale}` : ''; diff --git a/lib/routes/cw/author.ts b/lib/routes/cw/author.ts index 2be67a126dfe..fda1501a0996 100644 --- a/lib/routes/cw/author.ts +++ b/lib/routes/cw/author.ts @@ -27,11 +27,11 @@ export const route: Route = { }; async function handler(ctx) { - const browser = await playwright(); + const context = await playwright(); - const { $, items } = await parsePage('author', browser, ctx); + const { $, items } = await parsePage('author', context, ctx); - await browser.close(); + await context.close(); return { title: $('head title').text(), diff --git a/lib/routes/cw/master.ts b/lib/routes/cw/master.ts index eaae855f3f21..239d6c22a334 100644 --- a/lib/routes/cw/master.ts +++ b/lib/routes/cw/master.ts @@ -37,11 +37,11 @@ export const route: Route = { }; async function handler(ctx) { - const browser = await playwright(); + const context = await playwright(); - const { $, items } = await parsePage('master', browser, ctx); + const { $, items } = await parsePage('master', context, ctx); - await browser.close(); + await context.close(); return { title: $('head title').text(), diff --git a/lib/routes/cw/sub.ts b/lib/routes/cw/sub.ts index 3f5abdb728bb..a374c6d1921c 100644 --- a/lib/routes/cw/sub.ts +++ b/lib/routes/cw/sub.ts @@ -22,11 +22,11 @@ export const route: Route = { }; async function handler(ctx) { - const browser = await playwright(); + const context = await playwright(); - const { $, items } = await parsePage('sub', browser, ctx); + const { $, items } = await parsePage('sub', context, ctx); - await browser.close(); + await context.close(); return { title: $('head title').text(), diff --git a/lib/routes/cw/today.ts b/lib/routes/cw/today.ts index dd34056332ab..73e4303f4c0e 100644 --- a/lib/routes/cw/today.ts +++ b/lib/routes/cw/today.ts @@ -28,11 +28,11 @@ export const route: Route = { }; async function handler(ctx) { - const browser = await playwright(); + const context = await playwright(); - const { $, items } = await parsePage('today', browser, ctx); + const { $, items } = await parsePage('today', context, ctx); - await browser.close(); + await context.close(); return { title: $('head title').text(), diff --git a/lib/routes/cw/utils.ts b/lib/routes/cw/utils.ts index f1f9a9859505..e9935dc079fe 100644 --- a/lib/routes/cw/utils.ts +++ b/lib/routes/cw/utils.ts @@ -1,5 +1,6 @@ import { load } from 'cheerio'; +import { config } from '@/config'; import cache from '@/utils/cache'; import logger from '@/utils/logger'; import ofetch from '@/utils/ofetch'; @@ -29,13 +30,13 @@ const pathMap = { }, }; -const getCookie = async (browser, tryGet) => { +const getCookie = async (context, tryGet) => { if (!cookie) { cookie = await tryGet('cw:cookie', async () => { - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' ? route.continue() : route.abort(); }); logger.http(`Requesting ${baseUrl}/user/get/cookie-bar`); await page.goto(`${baseUrl}/user/get/cookie-bar`, { @@ -49,14 +50,14 @@ const getCookie = async (browser, tryGet) => { return cookie; }; -const parsePage = async (path, browser, ctx) => { +const parsePage = async (path, context, ctx) => { const pageUrl = `${baseUrl}${pathMap[path].pageUrl(ctx.req.param('channel'))}`; - const cookie = await getCookie(browser, cache.tryGet); - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + const cookie = await getCookie(context, cache.tryGet); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' ? route.continue() : route.abort(); }); await setCookies(page, cookie, 'cw.com.tw'); logger.http(`Requesting ${pageUrl}`); @@ -70,7 +71,7 @@ const parsePage = async (path, browser, ctx) => { const $ = load(response); const list = parseList($, ctx.req.query('limit') ? Number(ctx.req.query('limit')) : pathMap[path].limit); - const items = await parseItems(list, browser, cache.tryGet); + const items = await parseItems(list, context, cache.tryGet); return { $, items }; }; @@ -88,14 +89,14 @@ const parseList = ($, limit) => }) .slice(0, limit); -const parseItems = (list, browser, tryGet) => +const parseItems = (list, context, tryGet) => Promise.all( list.map((item) => tryGet(item.link, async () => { const response = await ofetch(item.link, { headers: { - Cookie: await getCookie(browser, tryGet), - 'User-Agent': browser.userAgent(), + Cookie: await getCookie(context, tryGet), + 'User-Agent': config.ua, }, }); const $ = load(response); @@ -103,10 +104,12 @@ const parseItems = (list, browser, tryGet) => const meta = JSON.parse($('head script[type="application/ld+json"]').eq(0).text()); $('.article__head .breadcrumb, .article__head h1, .article__provideViews, .ad').remove(); $('img.lazyload').each((_, img) => { - if (img.attribs['data-src']) { - img.attribs.src = img.attribs['data-src']; - delete img.attribs['data-src']; + if (!img.attribs['data-src']) { + return; } + + img.attribs.src = img.attribs['data-src']; + delete img.attribs['data-src']; }); item.title = $('head title').text(); diff --git a/lib/routes/cybersecurityventures/news.ts b/lib/routes/cybersecurityventures/news.ts index 29cb7e21858f..97936d5d57fe 100644 --- a/lib/routes/cybersecurityventures/news.ts +++ b/lib/routes/cybersecurityventures/news.ts @@ -59,18 +59,18 @@ export const route: Route = { categories: ['programming'], path: '/news/:category?', example: '/cybersecurityventures/news', - radar: Object.keys(categories).map((key) => ({ + radar: Object.entries(categories).map(([key, value]) => ({ source: [`cybersecurityventures.com/${key}`], target: `/news/${key}`, - title: categories[key].label, + title: value.label, })), parameters: { category: { description: 'news category', default: 'today', - options: Object.keys(categories).map((key) => ({ + options: Object.entries(categories).map(([key, value]) => ({ value: key, - label: categories[key].label, + label: value.label, })), }, }, @@ -88,7 +88,7 @@ async function handler(ctx: Context): Promise { const category = ctx.req.param('category') ?? 'today'; const limit = ctx.req.query('limit') ?? 20; - if (!(category in categories)) { + if (!Object.hasOwn(categories, category)) { throw new InvalidParameterError('Invalid category'); } diff --git a/lib/routes/cyzone/author.ts b/lib/routes/cyzone/author.ts index 995f3c128f3f..7c320b02905f 100644 --- a/lib/routes/cyzone/author.ts +++ b/lib/routes/cyzone/author.ts @@ -28,7 +28,7 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 5; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 5; const apiUrl = new URL('v2/author/author/detail', apiRootUrl).href; const currentUrl = new URL(`author/${id}`, rootUrl).href; diff --git a/lib/routes/cyzone/index.ts b/lib/routes/cyzone/index.ts index 5cde9e9751db..735150462e64 100644 --- a/lib/routes/cyzone/index.ts +++ b/lib/routes/cyzone/index.ts @@ -29,7 +29,7 @@ export const route: Route = { async function handler(ctx) { const { id = 'news' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 5; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 5; const apiUrl = new URL(`v2/content/channel/${id === 'news' ? 'getArticle' : 'detail'}`, apiRootUrl).href; const currentUrl = new URL(`channel/${id}`, rootUrl).href; diff --git a/lib/routes/cyzone/label.ts b/lib/routes/cyzone/label.ts index 47f45412058a..e9de525a6ba1 100644 --- a/lib/routes/cyzone/label.ts +++ b/lib/routes/cyzone/label.ts @@ -28,7 +28,7 @@ export const route: Route = { async function handler(ctx) { const name = ctx.req.param('name'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 5; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 5; const apiUrl = new URL('v2/content/tag/tagList', apiRootUrl).href; const currentUrl = new URL(`label/${name}`, rootUrl).href; diff --git a/lib/routes/cyzone/util.ts b/lib/routes/cyzone/util.ts index 2f61635d0a83..76f8a6c5ff4f 100644 --- a/lib/routes/cyzone/util.ts +++ b/lib/routes/cyzone/util.ts @@ -19,7 +19,7 @@ const getInfo = (url, tryGet) => const $ = load(response); - const avatar = $('img.avatar')?.prop('src')?.split('?')[0] ?? undefined; + const avatar = $('img.avatar')?.prop('src')?.split('?', 1)[0] ?? undefined; const icon = new URL($('link[rel="icon"]')?.prop('href'), rootUrl).href; const image = new URL($('div.logo img')?.prop('src'), rootUrl).href; @@ -91,7 +91,7 @@ const processItems = async (apiUrl, limit, tryGet, ...params) => { content('img').each((_, el) => { if (content(el).prop('src')) { - content(el).prop('src', content(el).prop('src').split('?')[0]); + content(el).prop('src', content(el).prop('src').split('?', 1)[0]); } else { content(el).remove(); } diff --git a/lib/routes/daily/discussed.ts b/lib/routes/daily/discussed.ts index 92973c8a7dae..6b012d0c88e9 100644 --- a/lib/routes/daily/discussed.ts +++ b/lib/routes/daily/discussed.ts @@ -93,10 +93,10 @@ export const route: Route = { }; async function handler(ctx) { - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false; const dateSort = ctx.req.param('dateSort') ? JSON.parse(ctx.req.param('dateSort')) : true; - const period = ctx.req.param('period') ? Number.parseInt(ctx.req.param('period'), 10) : 7; + const period = ctx.req.param('period') ? Number(ctx.req.param('period')) : 7; const link = `${baseUrl}/posts/discussed`; diff --git a/lib/routes/daily/popular.ts b/lib/routes/daily/popular.ts index 8360dc059315..8b48490beb64 100644 --- a/lib/routes/daily/popular.ts +++ b/lib/routes/daily/popular.ts @@ -144,7 +144,7 @@ export const route: Route = { async function handler(ctx) { const link = `${baseUrl}/posts`; - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 15; const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false; const dateSort = ctx.req.param('dateSort') ? JSON.parse(ctx.req.param('dateSort')) : true; diff --git a/lib/routes/daily/source.ts b/lib/routes/daily/source.ts index 0c9aba5a45e4..104b5fdebe64 100644 --- a/lib/routes/daily/source.ts +++ b/lib/routes/daily/source.ts @@ -147,7 +147,7 @@ export const route: Route = { async function handler(ctx) { const sourceId = ctx.req.param('sourceId'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10; const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false; const link = `${baseUrl}/sources/${sourceId}`; diff --git a/lib/routes/daily/squads.ts b/lib/routes/daily/squads.ts index 5bc199acaa9e..c71880192536 100644 --- a/lib/routes/daily/squads.ts +++ b/lib/routes/daily/squads.ts @@ -213,7 +213,7 @@ export const route: Route = { }; async function handler(ctx) { - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false; const squads = ctx.req.param('squads'); diff --git a/lib/routes/daily/upvoted.ts b/lib/routes/daily/upvoted.ts index 06885d298dd2..43f9f27238e3 100644 --- a/lib/routes/daily/upvoted.ts +++ b/lib/routes/daily/upvoted.ts @@ -153,10 +153,10 @@ export const route: Route = { async function handler(ctx) { const link = `${baseUrl}/posts/upvoted`; - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false; const dateSort = ctx.req.param('dateSort') ? JSON.parse(ctx.req.param('dateSort')) : true; - const period = ctx.req.param('period') ? Number.parseInt(ctx.req.param('period'), 10) : 7; + const period = ctx.req.param('period') ? Number(ctx.req.param('period')) : 7; const data = await getData({ query, diff --git a/lib/routes/daily/user.ts b/lib/routes/daily/user.ts index cbffaed740d5..d3eda7ba1698 100644 --- a/lib/routes/daily/user.ts +++ b/lib/routes/daily/user.ts @@ -160,7 +160,7 @@ export const route: Route = { async function handler(ctx) { const userId = ctx.req.param('userId'); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 7; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 7; const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false; const buildId = await getBuildId(); diff --git a/lib/routes/dailypush/all.ts b/lib/routes/dailypush/all.ts index 6a39fe133178..e92405045348 100644 --- a/lib/routes/dailypush/all.ts +++ b/lib/routes/dailypush/all.ts @@ -42,12 +42,12 @@ async function handler(ctx) { const { sort = '' } = ctx.req.param(); const url = sort ? `${BASE_URL}/${sort}` : BASE_URL; - const browser = await playwright(); + const context = await playwright(); try { - const html = await fetchPageHtml(browser, url, 'article'); + const html = await fetchPageHtml(context, url, 'article'); const $ = load(html); const list = parseArticles($, BASE_URL); - const items = await enhanceItemsWithSummaries(browser, list); + const items = await enhanceItemsWithSummaries(context, list); const pageTitle = $('title').text() || 'DailyPush - All'; @@ -57,6 +57,6 @@ async function handler(ctx) { item: items, }; } finally { - await browser.close(); + await context.close(); } } diff --git a/lib/routes/dailypush/tags.ts b/lib/routes/dailypush/tags.ts index c1430d29ae47..0cac69fc7345 100644 --- a/lib/routes/dailypush/tags.ts +++ b/lib/routes/dailypush/tags.ts @@ -43,12 +43,12 @@ async function handler(ctx) { const { tag, sort = 'trending' } = ctx.req.param(); const url = `${BASE_URL}/${tag}/${sort}`; - const browser = await playwright(); + const context = await playwright(); try { - const html = await fetchPageHtml(browser, url, 'article'); + const html = await fetchPageHtml(context, url, 'article'); const $ = load(html); const list = parseArticles($, BASE_URL); - const items = await enhanceItemsWithSummaries(browser, list); + const items = await enhanceItemsWithSummaries(context, list); const pageTitle = $('title').text() || `DailyPush - ${tag.charAt(0).toUpperCase() + tag.slice(1)}`; @@ -58,6 +58,6 @@ async function handler(ctx) { item: items, }; } finally { - await browser.close(); + await context.close(); } } diff --git a/lib/routes/dailypush/utils.ts b/lib/routes/dailypush/utils.ts index 1fd24ec3ad85..134eca52ff32 100644 --- a/lib/routes/dailypush/utils.ts +++ b/lib/routes/dailypush/utils.ts @@ -1,11 +1,12 @@ import type { CheerioAPI } from 'cheerio'; import { load } from 'cheerio'; +import type { BrowserContext } from 'patchright'; import type { DataItem } from '@/types'; import cache from '@/utils/cache'; import logger from '@/utils/logger'; import { parseRelativeDate } from '@/utils/parse-date'; -import type { Browser, Page } from '@/utils/playwright'; +import type { Page } from '@/utils/playwright'; export const BASE_URL = 'https://www.dailypush.dev'; @@ -23,19 +24,19 @@ export interface ArticleItem { const allowedRequestTypes = new Set(['document']); async function preparePage(page: Page) { - await page.setRequestInterception(true); - page.on('request', (request) => { + await page.route('**/*', (route) => { + const request = route.request(); if (allowedRequestTypes.has(request.resourceType())) { - request.continue(); + route.continue(); return; } - request.abort(); + route.abort(); }); } -export async function fetchPageHtml(browser: Browser, url: string, waitForSelector?: string): Promise { - const page = await browser.newPage(); +export async function fetchPageHtml(context: BrowserContext, url: string, waitForSelector?: string): Promise { + const page = await context.newPage(); await preparePage(page); try { @@ -143,7 +144,7 @@ function extractCategories(article: ReturnType, $: CheerioAPI): stri const tagText = tagElement.text().trim(); // Skip summary/stats links and navigation - if (tagHref && tagText && !tagHref.includes('article/') && !tagHref.includes('Summary') && tagText.length < 50 && !/^(Summary|stats|About|Tags|Toggle|Trending|Latest|Previous|Next)$/i.test(tagText)) { + if (tagHref && tagText && !tagHref.includes('article/') && !tagHref.includes('Summary') && tagText.length < 50 && !/^(?:Summary|stats|About|Tags|Toggle|Trending|Latest|Previous|Next)$/i.test(tagText)) { return tagText; } return null; @@ -259,9 +260,9 @@ export function parseArticles($: CheerioAPI, baseUrl: string): ArticleItem[] { /** * Enhance items with full summaries from dailypush article pages. - * Uses the provided browser; opens a new tab per URL (document requests only). Caller must close the browser. + * Uses the provided context; opens a new tab per URL (document requests only). Caller must close the context. */ -export async function enhanceItemsWithSummaries(browser: Browser, items: ArticleItem[]): Promise { +export async function enhanceItemsWithSummaries(context: BrowserContext, items: ArticleItem[]): Promise { const itemsWithUrl = items.filter((item) => item.dailyPushUrl !== undefined); const itemsWithoutUrl: DataItem[] = items.filter((item) => item.dailyPushUrl === undefined); @@ -269,7 +270,7 @@ export async function enhanceItemsWithSummaries(browser: Browser, items: Article itemsWithUrl.map((item) => cache.tryGet(item.dailyPushUrl!, async () => { try { - const html = await fetchPageHtml(browser, item.dailyPushUrl!, 'p.font-ibm-plex-sans.leading-relaxed'); + const html = await fetchPageHtml(context, item.dailyPushUrl!, 'p.font-ibm-plex-sans.leading-relaxed'); const $ = load(html); const summary = $('p.font-ibm-plex-sans.leading-relaxed'); if (summary.length > 0 && summary.text().trim()) { diff --git a/lib/routes/daum/potplayer.ts b/lib/routes/daum/potplayer.ts index 919afa2166b1..865d74b24ff0 100644 --- a/lib/routes/daum/potplayer.ts +++ b/lib/routes/daum/potplayer.ts @@ -7,7 +7,7 @@ import { parseDate } from '@/utils/parse-date'; export const handler = async (ctx: Context): Promise => { const { lang } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '500', 10); + const limit = Number(ctx.req.query('limit') ?? '500'); const baseUrl = 'https://t1.daumcdn.net'; const targetUrl: string = new URL(`potplayer/PotPlayer/v4/Update2/Update${lang ?? ''}.html`, baseUrl).href; @@ -20,14 +20,14 @@ export const handler = async (ctx: Context): Promise => { // Group 3: Trailing hyphens (unused, but for context) // Group 4: Update content // Uses global and multiline flags for all matches and line start/end anchors - const updateRegex = /^(-+)\s*\n(.*?)\s*\n(-+)\s*\n([\s\S]*?)(?=\n-{2,}|<\/p>)/gm; + const updateRegex = /^-+[^\S\n]*\n(.*)\r?\n-+[^\S\n]*\n([\s\S]*?)(?=\n-{2}|<\/p>)/gm; const items: DataItem[] = []; let match: RegExpExecArray | null; while ((match = updateRegex.exec(response)) !== null && items.length < limit) { - const headerLine: string | undefined = match[2].trim(); - const description: string | undefined = match[4].trim()?.replaceAll(/(\s[+-])/g, '
$1'); + const headerLine: string | undefined = match[1].trim(); + const description: string | undefined = match[2].trim()?.replaceAll(/(\s[+-])/g, '
$1'); let version = 'N/A'; let pubDateStr: string | undefined = undefined; @@ -48,9 +48,9 @@ export const handler = async (ctx: Context): Promise => { } else if (numericDateMatch && numericDateMatch[1]) { const rawDate = numericDateMatch[1]; if (rawDate.length === 6 && (version === rawDate || !specificDateMatch)) { - const year = Number.parseInt(rawDate.slice(0, 2), 10); - const month = Number.parseInt(rawDate.slice(2, 4), 10); - const day = Number.parseInt(rawDate.slice(4, 6), 10); + const year = Number(rawDate.slice(0, 2)); + const month = Number(rawDate.slice(2, 4)); + const day = Number(rawDate.slice(4, 6)); const fullYear = year < 70 ? 2000 + year : 1900 + year; pubDateStr = `${fullYear}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; } diff --git a/lib/routes/dayanzai/index.ts b/lib/routes/dayanzai/index.ts index abb37cb3ab5c..11db2149fedd 100644 --- a/lib/routes/dayanzai/index.ts +++ b/lib/routes/dayanzai/index.ts @@ -41,7 +41,7 @@ async function handler(ctx) { const response = await got.get(currentUrl); const $ = load(response.data); const lists = $('div.c-box > div > div.c-zx-list > ul > li'); - const reg = /日期:(.*?(\s\(.*?\))?)\s/; + const reg = /日期:(.*?(?:\s\(.*?\))?)\s/; const list = lists.toArray().map((item) => { item = $(item).find('div'); let date = reg.exec(item.find('div.r > p.other').text())[1]; diff --git a/lib/routes/dbaplus/new.ts b/lib/routes/dbaplus/new.ts index 7e877f78a079..c7c4352f30a9 100644 --- a/lib/routes/dbaplus/new.ts +++ b/lib/routes/dbaplus/new.ts @@ -14,7 +14,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { id = '9' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'https://dbaplus.cn'; const targetUrl: string = new URL(`news-${id}-1.html`, baseUrl).href; @@ -135,13 +135,13 @@ export const handler = async (ctx: Context): Promise => { const description: string = $('meta[name="description"]').attr('content') ?? ''; return { - title: $('title').text().split(/:/)[0], + title: $('title').text().split(/:/, 1)[0], description, link: targetUrl, item: items, allowEmpty: true, image: $('div.navbar-header img').attr('src'), - author: description.split(/:/)[0], + author: description.split(/:/, 1)[0], language, id: targetUrl, }; diff --git a/lib/routes/dblp/publication.ts b/lib/routes/dblp/publication.ts index fd2112ac059a..5326503d7205 100644 --- a/lib/routes/dblp/publication.ts +++ b/lib/routes/dblp/publication.ts @@ -32,9 +32,7 @@ async function handler(ctx) { // 发送 HTTP GET 请求到 API 并解构返回的数据对象 const { - result: { - hits: { hit: data }, - }, + result: { hits }, } = await ofetch('https://dblp.org/search/publ/api', { query: { q: field, @@ -45,6 +43,7 @@ async function handler(ctx) { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', }, }); + const { hit: data } = hits; // console.log(data); diff --git a/lib/routes/dcard/section.ts b/lib/routes/dcard/section.ts index a32f14582405..9c30bc07c7fa 100644 --- a/lib/routes/dcard/section.ts +++ b/lib/routes/dcard/section.ts @@ -26,7 +26,7 @@ export const route: Route = { async function handler(ctx) { const { type = 'latest', section = 'posts' } = ctx.req.param(); const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; - const browser = await playwright(); + const context = await playwright(); let link = 'https://www.dcard.tw/f'; let api = 'https://www.dcard.tw/service/api/v2'; @@ -48,10 +48,10 @@ async function handler(ctx) { title += '最新'; } - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' ? route.continue() : route.abort(); }); await page.setExtraHTTPHeaders({ referer: `https://www.dcard.tw/f/${section}`, @@ -60,7 +60,7 @@ async function handler(ctx) { await page.goto(`${api}&limit=100`); await page.waitForSelector('body > pre'); const response = await page.evaluate(() => document.querySelector('body > pre').textContent); - const cookies = await cache.tryGet('dcard:cookies', () => page.cookies(), 3600, false); + const cookies = await cache.tryGet('dcard:cookies', () => page.context().cookies(), 3600, false); await page.close(); const data = JSON.parse(response); @@ -76,8 +76,8 @@ async function handler(ctx) { })); // parse fulltext for first `limit` items - const result = await utils.ProcessFeed(items, cookies, browser, limit, cache); - await browser.close(); + const result = await utils.ProcessFeed(items, cookies, context, limit, cache); + await context.close(); return { title, diff --git a/lib/routes/dcard/utils.ts b/lib/routes/dcard/utils.ts index 083c736d6dd6..1862c50d0c7d 100644 --- a/lib/routes/dcard/utils.ts +++ b/lib/routes/dcard/utils.ts @@ -1,34 +1,33 @@ import pMap from 'p-map'; -const ProcessFeed = async (items, cookies, browser, limit, cache) => { +const ProcessFeed = async (items, cookies, context, limit, cache) => { let newCookies = []; const result = await pMap( items.slice(0, limit), async (i) => { const url = `https://www.dcard.tw/service/api/v2/posts/${i.id}`; const content = await cache.tryGet(`dcard:${i.id}`, async () => { - let response; // try catch 处理被删除的帖子 try { - const page = await browser.newPage(); - await page.setRequestInterception(true); - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' || request.resourceType() === 'fetch' || request.resourceType() === 'xhr' ? request.continue() : request.abort(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' || request.resourceType() === 'fetch' || request.resourceType() === 'xhr' ? route.continue() : route.abort(); }); await page.setExtraHTTPHeaders({ referer: `https://www.dcard.tw/f/${i.forumAlias}/p/${i.id}`, }); - await page.setCookie(...cookies); + await page.context().addCookies(cookies); await page.goto(url); await page.waitForSelector('body > pre'); - response = await page.evaluate(() => document.querySelector('body > pre').textContent); - newCookies = await page.cookies(); + const response = await page.evaluate(() => document.querySelector('body > pre').textContent); + newCookies = await page.context().cookies(); await page.close(); const data = JSON.parse(response); let body = data.content; body = body.replaceAll(/(?=https?:\/\/).*?(?<=\.(jpe?g|gif|png))/gi, (m) => ``); - body = body.replaceAll(/(?=https?:\/\/).*(??)$/gim, (m) => `${m}`); + body = body.replaceAll(/(?=https?:\/\/).+(??)$/gim, (m) => `${m}`); body = body.replaceAll('\n', '
'); return body; diff --git a/lib/routes/dcfever/utils.tsx b/lib/routes/dcfever/utils.tsx index 487067ee34a7..7045d8fa49d5 100644 --- a/lib/routes/dcfever/utils.tsx +++ b/lib/routes/dcfever/utils.tsx @@ -76,7 +76,7 @@ const parseItem = (item) => content.find('img').each((_, e) => { if (e.attribs.src?.includes('?')) { - e.attribs.src = e.attribs.src.split('?')[0]; + e.attribs.src = e.attribs.src.split('?', 1)[0]; } }); diff --git a/lib/routes/deadbydaylight/index.ts b/lib/routes/deadbydaylight/index.ts index acc689c0ba1d..06a32e6ad1c8 100644 --- a/lib/routes/deadbydaylight/index.ts +++ b/lib/routes/deadbydaylight/index.ts @@ -43,8 +43,8 @@ async function handler() { // { 0: node: { id, locale, slug, title, excerpt, image, published_at, article_category}} const items = await Promise.all( - Object.keys(articleMeta).map((id) => { - const content = articleMeta[id].node; + Object.values(articleMeta).map((edge) => { + const content = edge.node; const slug = content.slug; const dataUrl = `${baseUrl}/page-data/news/${slug}/page-data.json`; diff --git a/lib/routes/deadline/posts.tsx b/lib/routes/deadline/posts.tsx index dd9a5276988a..0d285abb2fc2 100644 --- a/lib/routes/deadline/posts.tsx +++ b/lib/routes/deadline/posts.tsx @@ -54,7 +54,7 @@ async function handler(ctx) { $('.c-lazy-image__img').each((_, img) => { img = $(img); if (img.attr('data-lazy-src')) { - img.attr('src', img.attr('data-lazy-src').split('?')[0]); + img.attr('src', img.attr('data-lazy-src').split('?', 1)[0]); img.removeAttr('data-lazy-src'); img.removeAttr('data-lazy-srcset'); } diff --git a/lib/routes/dealstreetasia/home.ts b/lib/routes/dealstreetasia/home.ts index e0959e5b0ef0..177bbf62e7ce 100644 --- a/lib/routes/dealstreetasia/home.ts +++ b/lib/routes/dealstreetasia/home.ts @@ -60,7 +60,7 @@ async function fetchPage() { link: item.post_url || item.link || '', description: item.post_excerpt || item.excerpt || '', pubDate: item.post_date ? new Date(item.post_date).toUTCString() : item.date ? new Date(item.date).toUTCString() : '', - category: item.category_link ? item.category_link.replaceAll(/(<([^>]+)>)/gi, '') : '', // Clean HTML if category_link exists + category: item.category_link ? item.category_link.replaceAll(/(<([^>]+)>)/g, '') : '', // Clean HTML if category_link exists image: item.image_url ? item.image_url.replace(/\?.*$/, '') : '', // Remove query parameters if image_url exists })); diff --git a/lib/routes/decrypt/index.ts b/lib/routes/decrypt/index.ts index 6d9896371dcc..e54e29fd0516 100644 --- a/lib/routes/decrypt/index.ts +++ b/lib/routes/decrypt/index.ts @@ -52,7 +52,7 @@ async function handler(ctx): Promise { const result = await extractFullText(item.link); return { title: item.title || 'Untitled', - link: item.link.split('?')[0], // Clean URL by removing query parameters + link: item.link.split('?', 1)[0], // Clean URL by removing query parameters pubDate: item.pubDate ? parseDate(item.pubDate) : undefined, description: result?.fullText ?? (item.content || ''), author: item.creator || 'Decrypt', @@ -66,7 +66,7 @@ async function handler(ctx): Promise { // Fallback to RSS content return { title: item.title || 'Untitled', - link: item.link.split('?')[0], + link: item.link.split('?', 1)[0], pubDate: item.pubDate ? parseDate(item.pubDate) : undefined, description: item.content || '', author: item.creator || 'Decrypt', @@ -89,7 +89,7 @@ async function handler(ctx): Promise { } as Data; } -async function extractFullText(url: string): Promise<{ fullText: string; featuredImage: string; tags: string[] } | null> { +async function extractFullText(url: string): Promise { try { const response = await ofetch(url); diff --git a/lib/routes/dedao/articles.ts b/lib/routes/dedao/articles.ts index aa618daa4d5f..fab29eb49590 100644 --- a/lib/routes/dedao/articles.ts +++ b/lib/routes/dedao/articles.ts @@ -7,7 +7,7 @@ import got from '@/utils/got'; export const route: Route = { path: '/articles/:id?', categories: ['new-media'], - example: '/articles/9', // 示例路径更新 + example: '/dedao/articles/9', parameters: { id: '文章类型 ID,8 为得到头条,9 为得到精选,默认为 8' }, features: { requireConfig: false, diff --git a/lib/routes/dedao/index.ts b/lib/routes/dedao/index.ts index 773cf893b1d1..641932b74cea 100644 --- a/lib/routes/dedao/index.ts +++ b/lib/routes/dedao/index.ts @@ -33,7 +33,7 @@ async function handler(ctx) { let items = (category === 'news' ? data.news : category === 'figure' ? data.figure : data.videoList).map((item) => ({ title: item.title, pubDate: parseDate(item.online_time), - link: `${rootUrl}/${category === 'news' ? 'article/' : category === 'figure' ? 'people/' : ''}${item.online_time.split('T')[0].split('-').join('')}/${item.token}`, + link: `${rootUrl}/${category === 'news' ? 'article/' : category === 'figure' ? 'people/' : ''}${item.online_time.split('T', 1)[0].split('-').join('')}/${item.token}`, })); items = await Promise.all( diff --git a/lib/routes/dedao/knowledge.tsx b/lib/routes/dedao/knowledge.tsx index 076b8e9a7ba4..d5eb7c28c293 100644 --- a/lib/routes/dedao/knowledge.tsx +++ b/lib/routes/dedao/knowledge.tsx @@ -5,7 +5,7 @@ import type { Route } from '@/types'; import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; -const mentionPattern = /<\u2267\u2746>{"name":"(.*?)","uid":"\d+","at":"1"}<\/\u2266\u2746>/g; +const mentionPattern = /<\u2267\u2746>\{"name":"(.*?)","uid":"\d+","at":"1"\}<\/\u2266\u2746>/g; const formatNoteText = (text = '') => text.replaceAll('\n\n', '

').replaceAll(mentionPattern, ' @$1'); diff --git a/lib/routes/dedao/list.ts b/lib/routes/dedao/list.ts index ae6c6ad2daf9..9a1747174a53 100644 --- a/lib/routes/dedao/list.ts +++ b/lib/routes/dedao/list.ts @@ -45,7 +45,7 @@ async function handler(ctx) { url: listUrl, }); - const currentUrl = `${rootUrl}${listResponse.data.match(new RegExp('' + category + String.raw`<\/span>

relevant config'); } - return config.discourse.config[ctx.req.param('configId')]; + return discourseConfig; } export { getConfig }; diff --git a/lib/routes/discuz/discuz.ts b/lib/routes/discuz/discuz.ts index e39d4f063eed..cd1e59c37eef 100644 --- a/lib/routes/discuz/discuz.ts +++ b/lib/routes/discuz/discuz.ts @@ -34,7 +34,7 @@ async function fetchWithAntiBot(url: string, header: Record) { if (initialHtml.includes('document.location.reload()')) { const setCookies = response.headers.getSetCookie?.() ?? []; - const cookieStr = setCookies.map((c) => c.split(';')[0]).join('; '); + const cookieStr = setCookies.map((c) => c.split(';', 1)[0]).join('; '); if (cookieStr) { response = await ofetch.raw(url, { method: 'get', @@ -104,7 +104,7 @@ async function handler(ctx) { // 若没有指定编码,则默认utf-8 const contentType = response.headers['content-type'] || ''; let $ = load(iconv.decode(responseData, 'utf-8')); - const charset = contentType.match(/charset=([^;]*)/)?.[1] ?? $('meta[charset]').attr('charset') ?? $('meta[http-equiv="Content-Type"]').attr('content')?.split('charset=')?.[1]; + const charset = contentType.match(/charset=([^;]*)/)?.[1] ?? $('meta[charset]').attr('charset') ?? $('meta[http-equiv="Content-Type"]').attr('content')?.split('charset=', 2)?.[1]; if (charset?.toLowerCase() !== 'utf-8') { $ = load(iconv.decode(responseData, charset ?? 'utf-8')); } @@ -120,7 +120,7 @@ async function handler(ctx) { // discuz 7.x 系列 // 支持全文抓取,限制抓取页面5个 const list = $('tbody[id^="normalthread"] > tr') - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 5) + .slice(0, ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 5) .toArray() .map((item) => { item = $(item); @@ -147,7 +147,7 @@ async function handler(ctx) { // discuz X 系列 // 支持全文抓取,限制抓取页面5个 const list = $('tbody[id^="normalthread"] > tr') - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 5) + .slice(0, ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 5) .toArray() .map((item) => { item = $(item); diff --git a/lib/routes/dlnews/category.tsx b/lib/routes/dlnews/category.tsx index a30277817a10..8f331c1ef7c2 100644 --- a/lib/routes/dlnews/category.tsx +++ b/lib/routes/dlnews/category.tsx @@ -69,12 +69,13 @@ const extractArticle = (item) => const { data: response } = await got(item.link); const $ = load(response); const scriptTagContent = $('script#fusion-metadata').text(); - const jsonData = JSON.parse(scriptTagContent.match(/Fusion\.globalContent=({.*?});Fusion\.globalContentConfig/)[1]).content_elements; + const jsonData = JSON.parse(scriptTagContent.match(/Fusion\.globalContent=(\{.*?\});Fusion\.globalContentConfig/)[1]).content_elements; const filteredData = []; for (const v of jsonData) { if (v.type === 'header' && v.content.includes('What we’re reading')) { break; - } else if (v.type === 'custom_embed' && Boolean(v.embed.config.text)) { + } + if (v.type === 'custom_embed' && Boolean(v.embed.config.text)) { filteredData.push({ type: v.type, data: v.embed.config.text }); } else if (v.type === 'text' && !v.content.includes('NOW READ: ')) { filteredData.push({ type: v.type, data: v.content }); diff --git a/lib/routes/dmzj/news.ts b/lib/routes/dmzj/news.ts index a99b1594a00f..90fce6d40e1d 100644 --- a/lib/routes/dmzj/news.ts +++ b/lib/routes/dmzj/news.ts @@ -45,7 +45,7 @@ async function handler(ctx) { .map((item) => ({ title: $(item).find('h3 a').text(), link: $(item).find('h3 a').attr('href'), - author: $(item).find('.head_con_p_o span:nth-child(3)').text().split(':')[1], + author: $(item).find('.head_con_p_o span:nth-child(3)').text().split(':', 2)[1], pubDate: timezone(parseDate($(item).find('.head_con_p_o span').first().text(), 'YYYY-MM-DD HH:mm'), +8), description: $(item).find('p.com_about').text(), category: $(item) diff --git a/lib/routes/dn/news.ts b/lib/routes/dn/news.ts index 5d70ba919a40..fd20461d61ee 100644 --- a/lib/routes/dn/news.ts +++ b/lib/routes/dn/news.ts @@ -42,7 +42,7 @@ export const route: Route = { async function handler(ctx) { const { language, category = '' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10; const rootUrl = 'https://dn.com'; const currentUrl = new URL(`/${language}/news/${category}`, rootUrl).href; diff --git a/lib/routes/dnaindia/common.ts b/lib/routes/dnaindia/common.ts index e501d9b50378..180919d42009 100644 --- a/lib/routes/dnaindia/common.ts +++ b/lib/routes/dnaindia/common.ts @@ -44,7 +44,7 @@ export async function handler(ctx) { .map((item) => $(item).find('a').text()); // Process date const timeText = $('p.dna-update').text(); - const dateMatch = timeText.match(/Updated\s*:\s*([\w\s,:\d]+?)(?:\s*\||$)/); + const dateMatch = timeText.match(/Updated\s*:([\w\s,:]+)/); let time = dateMatch ? dateMatch[1].trim() : ''; time = time.replace(/\s+IST$/, ''); const pubDate = timezone(parseDate(time), +5.5); diff --git a/lib/routes/dnaindia/news.ts b/lib/routes/dnaindia/news.ts index 51191db282ca..80c055e6c91b 100644 --- a/lib/routes/dnaindia/news.ts +++ b/lib/routes/dnaindia/news.ts @@ -5,7 +5,7 @@ import { handler } from './common'; export const route: Route = { name: 'News', maintainers: ['Rjnishant530'], - path: ['/:category'], + path: '/:category', example: '/dnaindia/headlines', parameters: { category: 'Find it in the URL, or tables below', diff --git a/lib/routes/dnaindia/topic.ts b/lib/routes/dnaindia/topic.ts index 0a20bee67000..bf3f6d804d0a 100644 --- a/lib/routes/dnaindia/topic.ts +++ b/lib/routes/dnaindia/topic.ts @@ -5,7 +5,7 @@ import { handler } from './common'; export const route: Route = { name: 'Topic', maintainers: ['Rjnishant530'], - path: ['/topic/:topic'], + path: '/topic/:topic', example: '/dnaindia/topic/dna-verified', parameters: { category: 'Find it in the URL', diff --git a/lib/routes/domp4/detail.ts b/lib/routes/domp4/detail.ts index ddeaf4515008..a8500872b358 100644 --- a/lib/routes/domp4/detail.ts +++ b/lib/routes/domp4/detail.ts @@ -30,7 +30,7 @@ function getDomList($, detailUrl) { export function getItemList($, detailUrl, second) { const encoded = $('.article script[type]') .text() - .match(/return p}\('(.*)',(\d+),(\d+),'(.*)'.split\(/); + .match(/return p\}\('(.*)',(\d+),(\d+),'(.*)'.split\(/); // 若 script 标签没有内容,直接解析 dom if (!encoded) { return getDomList($, detailUrl); diff --git a/lib/routes/domp4/utils.ts b/lib/routes/domp4/utils.ts index c01cc48142c6..7e446b8b8c84 100644 --- a/lib/routes/domp4/utils.ts +++ b/lib/routes/domp4/utils.ts @@ -1,4 +1,3 @@ -/* eslint-disable unicorn/prefer-code-point */ import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; @@ -62,9 +61,9 @@ function getUrlType(url) { */ function decodeCipherText(p, a, c, k, e, d) { e = function (c) { - return (c < a ? '' : e(Number.parseInt((c / a).toString()))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36)); + return (c < a ? '' : e(Number.parseInt((c / a).toString()))) + ((c = c % a) > 35 ? String.fromCodePoint(c + 29) : c.toString(36)); }; - if (!''.replace(/^/, String)) { + if (!''.replace(/^/, () => '')) { while (c--) { d[e(c.toString())] = k[c] || e(c.toString()); } @@ -79,8 +78,10 @@ function decodeCipherText(p, a, c, k, e, d) { c = 1; } while (c--) { - if (k[c]) { - p = p.replaceAll(new RegExp(String.raw`\b` + e(c.toString()) + String.raw`\b`, 'g'), k[c]); + const replacement = k[c]; + if (replacement) { + const token = e(c.toString()); + p = p.replaceAll(new RegExp(String.raw`\b` + token + String.raw`\b`, 'g'), () => replacement); } } return p; diff --git a/lib/routes/dongqiudi/utils.ts b/lib/routes/dongqiudi/utils.ts index c927dd76828b..2b27887d450d 100644 --- a/lib/routes/dongqiudi/utils.ts +++ b/lib/routes/dongqiudi/utils.ts @@ -59,7 +59,7 @@ const ProcessImg = (content) => { delete img.attribs['orig-src']; delete img.attribs['data-src']; } - img.attribs.src = img.attribs.src.includes('?watermark') ? img.attribs.src.split('?watermark')[0] : img.attribs.src; + img.attribs.src = img.attribs.src.includes('?watermark') ? img.attribs.src.split('?watermark', 1)[0] : img.attribs.src; }); }; @@ -145,7 +145,7 @@ const ProcessFeedType3 = (item, response) => { const initialState = JSON.parse( $('script:contains("window.__INITIAL_STATE__")') .text() - .match(/window\.__INITIAL_STATE__\s*=\s*(.*?);\(/)[1] + .match(/window\.__INITIAL_STATE__\s*=\s*((?:\S.*?)??);\(/)[1] ); // filter out undefined item diff --git a/lib/routes/dora-world/article.ts b/lib/routes/dora-world/article.ts index c903ee1cce78..dbf80b6a0121 100644 --- a/lib/routes/dora-world/article.ts +++ b/lib/routes/dora-world/article.ts @@ -70,7 +70,7 @@ async function handler(ctx): Promise { return item; }) ) - ).then((items) => items.filter((item) => item !== null))) as DataItem[], + )) as DataItem[], }; } @@ -85,6 +85,6 @@ async function getContent(nextBuildId: string, contentId: string) { content .html() ?.replaceAll(rubyRegex, '$1($2)') - ?.replaceAll(/[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]/gm, '') ?? ''; + ?.replaceAll(/[^\t\n\r\u0020-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]/g, '') ?? ''; return description; } diff --git a/lib/routes/douban/channel/topic.ts b/lib/routes/douban/channel/topic.ts index c91dbf5cbe15..673e06a97a28 100644 --- a/lib/routes/douban/channel/topic.ts +++ b/lib/routes/douban/channel/topic.ts @@ -75,9 +75,8 @@ async function handler(ctx) { pubDate: new Date(item.create_time), link: item.url, }; - } else { - return null; } + return null; }) .filter(Boolean), allowEmpty: true, diff --git a/lib/routes/douban/other/classification.ts b/lib/routes/douban/other/classification.ts index 56b0c2f36dce..8549bc8308d2 100644 --- a/lib/routes/douban/other/classification.ts +++ b/lib/routes/douban/other/classification.ts @@ -26,7 +26,7 @@ export const route: Route = { async function handler(ctx) { const sort = ctx.req.param('sort') || 'U'; - const score = Number.parseFloat(ctx.req.param('score')) || 0; + const score = Number(ctx.req.param('score')) || 0; const tags = ctx.req.param('tags') || ''; const response = await got({ @@ -41,7 +41,7 @@ async function handler(ctx) { link: 'https://movie.douban.com/tag/#/?sort=U&range=0,10&tags=', item: movies .map((item) => { - const itemScore = Number.parseFloat(item.rate) || 0; + const itemScore = Number(item.rate) || 0; return itemScore >= score ? { diff --git a/lib/routes/douban/other/doulist.ts b/lib/routes/douban/other/doulist.ts index 77bf929e9fed..549083679dd5 100644 --- a/lib/routes/douban/other/doulist.ts +++ b/lib/routes/douban/other/doulist.ts @@ -52,7 +52,7 @@ async function handler(ctx) { description = $(item).find('span.status-recommend-text').text().trim(); } - if (type === '来自:豆瓣电影' || type === '来自:豆瓣' || type === '来自:豆瓣读书' || type === '来自:豆瓣音乐') { + if (['来自:豆瓣电影', '来自:豆瓣', '来自:豆瓣读书', '来自:豆瓣音乐'].includes(type)) { title = $(item).find('div.bd.doulist-subject div.title a').text().trim(); link = $(item).find('div.bd.doulist-subject div.title a').attr('href'); diff --git a/lib/routes/douban/other/playing.ts b/lib/routes/douban/other/playing.ts index 944c4b8a4f33..750ef4cd6373 100644 --- a/lib/routes/douban/other/playing.ts +++ b/lib/routes/douban/other/playing.ts @@ -22,7 +22,7 @@ export const route: Route = { }; async function handler(ctx) { - const score = Number.parseFloat(ctx.req.param('score')) || 0; + const score = Number(ctx.req.param('score')) || 0; const response = await got({ method: 'get', url: 'https://movie.douban.com/cinema/nowplaying/beijing', @@ -36,7 +36,7 @@ async function handler(ctx) { .toArray() .map((i) => { const item = $(i); - const itemScore = Number.parseFloat(item.attr('data-score')) || 0; + const itemScore = Number(item.attr('data-score')) || 0; return itemScore >= score ? { title: item.attr('data-title'), diff --git a/lib/routes/douban/other/replied.ts b/lib/routes/douban/other/replied.ts index 23767e6ce856..c231d97ee642 100644 --- a/lib/routes/douban/other/replied.ts +++ b/lib/routes/douban/other/replied.ts @@ -56,7 +56,7 @@ async function handler(ctx) { method: 'get', url: item.link, }); - const match = detailResponse.data.match(/'comments':(.*)}],/); + const match = detailResponse.data.match(/'comments':(.*)\}\],/); if (match.length > 1) { const content = load(detailResponse.data); diff --git a/lib/routes/douban/other/replies.ts b/lib/routes/douban/other/replies.ts index 7d2515a6137a..098f5509e922 100644 --- a/lib/routes/douban/other/replies.ts +++ b/lib/routes/douban/other/replies.ts @@ -55,10 +55,10 @@ async function handler(ctx) { url: item.link, }); - const comments = JSON.parse(detailResponse.data.match(/'comments':(.*)}],/)[1] + '}]'); + const comments = JSON.parse(detailResponse.data.match(/'comments':(.*)\}\],/)[1] + '}]'); for (const c of comments) { - if (c.id === item.link.split('#')[1]) { + if (c.id === item.link.split('#', 2)[1]) { return { link: item.link, title: `${c.author.name} 于 ${c.create_time} 的回应`, @@ -66,7 +66,8 @@ async function handler(ctx) { description: c.text, author: c.author.name, }; - } else if (c.replies.length > 0) { + } + if (c.replies.length > 0) { comments.push(...c.replies); } } diff --git a/lib/routes/douban/other/topic.ts b/lib/routes/douban/other/topic.ts index e5c56ea4b36f..86c853bc9079 100644 --- a/lib/routes/douban/other/topic.ts +++ b/lib/routes/douban/other/topic.ts @@ -54,7 +54,7 @@ async function handler(ctx) { let link; let title; if (type === 'status') { - link = item.target.status.sharing_url.split('&')[0]; + link = item.target.status.sharing_url.split('&', 1)[0]; author = item.target.status.author.name; title = author + '的广播'; date = item.target.status.create_time; diff --git a/lib/routes/douban/people/status.ts b/lib/routes/douban/people/status.ts index f5a225d4fee4..cf7b8cdcd284 100644 --- a/lib/routes/douban/people/status.ts +++ b/lib/routes/douban/people/status.ts @@ -101,7 +101,7 @@ function tryFixStatus(status) { } if (status.sharing_url) { - status.sharing_url = status.sharing_url.split('&')[0]; + status.sharing_url = status.sharing_url.split('&', 1)[0]; } if (!result.isFixSuccess) { @@ -313,10 +313,7 @@ function getContentByActivity(ctx, item, params = {}, picsPrefixes = []) { } picsPrefixes.push(picsPrefix); - const imageUrls: Array = []; - for (const image of status.images) { - imageUrls.push(image?.large?.url); - } + const imageUrls: Array = Array.from(status.images, (image) => image?.large?.url); description += prepareImages(imageUrls); } @@ -412,10 +409,7 @@ function getContentByActivity(ctx, item, params = {}, picsPrefixes = []) { description += '

'; } if (status.card.images_block) { - const imageUrls: Array = []; - for (const image of status.card.images_block.images) { - imageUrls.push(image.image?.large?.url); - } + const imageUrls: Array = Array.from(status.card.images_block.images, (image) => image.image?.large?.url); description += prepareImages(imageUrls); } } diff --git a/lib/routes/douban/people/wish.ts b/lib/routes/douban/people/wish.ts index bd676988bb78..703767c573cb 100644 --- a/lib/routes/douban/people/wish.ts +++ b/lib/routes/douban/people/wish.ts @@ -1,5 +1,3 @@ -import querystring from 'node:querystring'; - import { load } from 'cheerio'; import { config } from '@/config'; @@ -8,10 +6,10 @@ import cache from '@/utils/cache'; import got from '@/utils/got'; export const route: Route = { - path: '/people/:userid/wish/:routeParams?', + path: '/people/:userid/wish', categories: ['social-media'], example: '/douban/people/exherb/wish', - parameters: { userid: '用户id', routeParams: '额外参数;见下' }, + parameters: { userid: '用户id' }, features: { requireConfig: false, requirePuppeteer: false, @@ -23,76 +21,51 @@ export const route: Route = { name: '用户想看', maintainers: ['exherb'], handler, - description: `对于豆瓣用户想看的内容,在 \`routeParams\` 参数中以 query string 格式设置如下选项可以控制输出的样式 - -| 键 | 含义 | 接受的值 | 默认值 | -| ---------- | ---------- | -------- | ------ | -| pagesCount | 查询页面数 | | 1 |`, }; async function handler(ctx) { const userid = ctx.req.param('userid'); - const routeParams = querystring.parse(ctx.req.param('routeParams')); - - let userName; - - const pageSize = 15; - const pagesCount = routeParams.pagesCount ? Number.parseInt(routeParams.pagesCount) : 1; - const tasks = []; - for (let page = 0; page < pagesCount; page += 1) { - const url = `https://movie.douban.com/people/${userid}/wish?start=${page * pageSize}`; - - tasks.push( - cache - .tryGet( - url, - async () => { - const _r = await got({ - method: 'GET', - url, - headers: { - Referer: url, - Cookie: config.douban.cookie || '', - }, - }); - return _r.data; - }, - config.cache.routeExpire, - false - ) - .then((data) => { - const $ = load(data); - const list = $('div.article > div.grid-view > div.item'); - userName = userName || $('div.side-info-txt > h3').text(); - if (list) { - return Promise.all( - list.toArray().map((item) => { - item = $(item); - const itemPicUrl = item.find('.pic a img').attr('src'); - const info = item.find('.info'); - const title = info.find('ul li.title a').text(); - const url = info.find('ul li.title a').attr('href'); - const title_ = title.split('/').find((title) => title.trim()); - const day = info.find('ul li .date').text().trim(); - const rssItem = { - title: title_, - description: `${info.find('.intro').text()}
`, - link: url, - pubDate: new Date(day), - }; + const url = `https://movie.douban.com/people/${userid}/wish`; + const data = await cache.tryGet( + url, + async () => { + const _r = await got({ + method: 'GET', + url, + headers: { + Referer: url, + Cookie: config.douban.cookie || '', + }, + }); + return _r.data; + }, + config.cache.routeExpire, + false + ); + const $ = load(data); + const username = $('div.side-info-txt > h3').text(); - return rssItem; - }) - ); - } - }) - ); - } + const items = $('div.article > div.grid-view > div.item') + .toArray() + .map((item) => { + item = $(item); + const itemPicUrl = item.find('.pic a img').attr('src'); + const info = item.find('.info'); + const title = info.find('ul li.title a').text(); + const url = info.find('ul li.title a').attr('href'); + const title_ = title.split('/').find((title) => title.trim()); + const day = info.find('ul li .date').text().trim(); + return { + title: title_, + description: `${info.find('.intro').text()}
`, + link: url, + pubDate: new Date(day), + }; + }); - const items = (await Promise.all(tasks)).flat(); return { - title: `豆瓣想看 - ${userName || userid}`, + title: `豆瓣想看 - ${username || userid}`, link: `https://movie.douban.com/people/${userid}/wish`, item: items, }; diff --git a/lib/routes/douban/tv/coming.ts b/lib/routes/douban/tv/coming.ts index 1b4581383b28..33fa97c18ff1 100644 --- a/lib/routes/douban/tv/coming.ts +++ b/lib/routes/douban/tv/coming.ts @@ -55,27 +55,27 @@ const getPubDate = (pubdate?: string[]): Date | undefined => { return undefined; } - const datePart = pubDateText.split('(')[0]; + const datePart = pubDateText.split('(', 1)[0]; return parseDate(datePart); }; const getSortTimestamp = (pubdate?: string[]): number => { const pubDateText = getPubDateText(pubdate); if (!pubDateText) { - return Number.POSITIVE_INFINITY; + return Infinity; } - const datePart = pubDateText.split('(')[0].trim(); + const datePart = pubDateText.split('(', 1)[0].trim(); const match = /^(\d{4})(?:-(\d{1,2}))?(?:-(\d{1,2}))?/.exec(datePart); if (!match) { - return Number.POSITIVE_INFINITY; + return Infinity; } - const year = Number.parseInt(match[1], 10); - const month = match[2] ? Number.parseInt(match[2], 10) : 1; - const day = match[3] ? Number.parseInt(match[3], 10) : 1; + const year = Number(match[1]); + const month = match[2] ? Number(match[2]) : 1; + const day = match[3] ? Number(match[3]) : 1; const timestamp = Date.UTC(year, month - 1, day); - return Number.isNaN(timestamp) ? Number.POSITIVE_INFINITY : timestamp; + return Number.isNaN(timestamp) ? Infinity : timestamp; }; const getWishCount = (wishCount?: number | string): number => { @@ -83,7 +83,7 @@ const getWishCount = (wishCount?: number | string): number => { return wishCount; } if (typeof wishCount === 'string') { - const parsed = Number.parseInt(wishCount, 10); + const parsed = Number(wishCount); return Number.isNaN(parsed) ? 0 : parsed; } return 0; @@ -146,7 +146,7 @@ async function handler(ctx) { const countParam = ctx.req.param('count'); const sortBy = sortByParam === 'time' ? 'time' : 'hot'; - const rawCount = Number.parseInt(countParam || '', 10); + const rawCount = Number(countParam || ''); const requestCount = Number.isNaN(rawCount) || rawCount <= 0 ? 10 : rawCount; const ts = new Date().toISOString().slice(0, 10).replaceAll('-', ''); diff --git a/lib/routes/douyin/hashtag.ts b/lib/routes/douyin/hashtag.ts index 47f3f01fa9e3..77541e05e406 100644 --- a/lib/routes/douyin/hashtag.ts +++ b/lib/routes/douyin/hashtag.ts @@ -47,12 +47,12 @@ async function handler(ctx) { const tagData = await cache.tryGet( `douyin:hashtag:${cid}`, async () => { - const browser = await playwright(); - const page = await browser.newPage(); - await page.setRequestInterception(true); + const context = await playwright(); + const page = await context.newPage(); let awemeList = ''; - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' || request.resourceType() === 'xhr' ? request.continue() : request.abort(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' || request.resourceType() === 'xhr' ? route.continue() : route.abort(); }); page.on('response', async (response) => { const request = response.request(); @@ -61,11 +61,11 @@ async function handler(ctx) { } }); await page.goto(tagUrl, { - waitUntil: 'networkidle2', + waitUntil: 'networkidle', }); await page.waitForSelector('#RENDER_DATA'); const html = await page.evaluate(() => document.querySelector('#RENDER_DATA').textContent); - await browser.close(); + await context.close(); const renderData = JSON.parse(decodeURIComponent(html)); const dataKey = Object.keys(renderData).find((key) => renderData[key].topicDetail); diff --git a/lib/routes/douyin/live.ts b/lib/routes/douyin/live.ts index c1d3f7a438b4..93cb864a36f3 100644 --- a/lib/routes/douyin/live.ts +++ b/lib/routes/douyin/live.ts @@ -42,12 +42,11 @@ async function handler(ctx) { `douyin:live:${rid}`, async () => { let roomInfo; - const browser = await playwright(); - const page = await browser.newPage(); - await page.setRequestInterception(true); - - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'stylesheet' || request.resourceType() === 'script' || request.resourceType() === 'xhr' ? request.continue() : request.abort(); + const context = await playwright(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'stylesheet' || request.resourceType() === 'script' || request.resourceType() === 'xhr' ? route.continue() : route.abort(); }); page.on('response', async (response) => { const request = response.request(); @@ -57,9 +56,9 @@ async function handler(ctx) { }); logger.http(`Requesting ${pageUrl}`); await page.goto(pageUrl, { - waitUntil: 'networkidle2', + waitUntil: 'networkidle', }); - await browser.close(); + await context.close(); return roomInfo; }, diff --git a/lib/routes/douyin/user.ts b/lib/routes/douyin/user.ts index bbfbb8edc50f..ca0a020e8ed5 100644 --- a/lib/routes/douyin/user.ts +++ b/lib/routes/douyin/user.ts @@ -50,12 +50,11 @@ async function handler(ctx) { `douyin:user:${uid}`, async () => { let postData; - const browser = await playwright(); - const page = await browser.newPage(); - await page.setRequestInterception(true); - - page.on('request', (request) => { - request.resourceType() === 'document' || request.resourceType() === 'script' || request.resourceType() === 'xhr' ? request.continue() : request.abort(); + const context = await playwright(); + const page = await context.newPage(); + await page.route('**/*', (route) => { + const request = route.request(); + request.resourceType() === 'document' || request.resourceType() === 'script' || request.resourceType() === 'xhr' ? route.continue() : route.abort(); }); page.on('response', async (response) => { const request = response.request(); @@ -66,10 +65,10 @@ async function handler(ctx) { logger.http(`Requesting ${pageUrl}`); await page.goto(pageUrl, { - waitUntil: 'networkidle2', + waitUntil: 'networkidle', }); - await browser.close(); + await context.close(); if (!postData) { throw new Error('Empty post data. The request may be filtered by WAF.'); @@ -114,7 +113,7 @@ async function handler(ctx) { const description = templates.desc({ desc, media }); return { - title: post.desc.split('\n')[0], + title: post.desc.split('\n', 1)[0], description, link: `https://www.douyin.com/video/${post.aweme_id}`, pubDate: parseDate(post.create_time * 1000), diff --git a/lib/routes/douyin/utils.ts b/lib/routes/douyin/utils.ts index 4776659d12c9..a08dd4765bbf 100644 --- a/lib/routes/douyin/utils.ts +++ b/lib/routes/douyin/utils.ts @@ -32,12 +32,11 @@ const proxyVideo = (url, proxy) => { proxy += '='; } return proxy + encodeURIComponent(url); - } else { - if (!proxy.endsWith('/')) { - proxy += '/'; - } - return proxy + url; } + if (!proxy.endsWith('/')) { + proxy += '/'; + } + return proxy + url; }; const getOriginAvatar = (url) => diff --git a/lib/routes/dpm/exhibitions.tsx b/lib/routes/dpm/exhibitions.tsx index e33df05dce79..5aa2a3de4782 100644 --- a/lib/routes/dpm/exhibitions.tsx +++ b/lib/routes/dpm/exhibitions.tsx @@ -66,7 +66,7 @@ export const route: Route = { }); const museumName = namespace.zh?.name || namespace.name; - const titleTag = currentType ? `${currentType.name}` : '正在展览'; + const titleTag = currentType ? currentType.name : '正在展览'; const $ = load(response.data); const itemElements = $('.item').toArray(); diff --git a/lib/routes/dribbble/utils.tsx b/lib/routes/dribbble/utils.tsx index c7438b556977..727b3836e658 100644 --- a/lib/routes/dribbble/utils.tsx +++ b/lib/routes/dribbble/utils.tsx @@ -17,7 +17,7 @@ async function loadContent(link) { const shotData = JSON.parse( $('script') .text() - .match(/shotData:\s({.+?}),\n/)?.[1] ?? '{}' + .match(/shotData:\s(\{.+?\}),\n/)?.[1] ?? '{}' ); // Join multiple shots together by selecting elements with class 'media-shot' or 'main-shot' or 'block-media-wrapper' @@ -51,11 +51,11 @@ async function loadContent(link) { } if (!img.attr('src') && img.data('src')) { - img.attr('src', img.data('src').split('?')[0]); + img.attr('src', img.data('src').split('?', 1)[0]); img.removeAttr('data-src'); } - img.attr('src', img.attr('src').split('?')[0]); + img.attr('src', img.attr('src').split('?', 1)[0]); img.removeAttr('srcset'); img.removeAttr('data-srcset'); }); diff --git a/lib/routes/duozhi/index.ts b/lib/routes/duozhi/index.ts index 8f4358529458..1e873ea0ccd0 100644 --- a/lib/routes/duozhi/index.ts +++ b/lib/routes/duozhi/index.ts @@ -14,7 +14,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { const { category } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + const limit = Number(ctx.req.query('limit') ?? '30'); const baseUrl = 'http://www.duozhi.com'; const targetUrl: string = new URL(category && category.endsWith('/') ? category : category ? `${category}/` : '', baseUrl).href; @@ -45,7 +45,7 @@ export const handler = async (ctx: Context): Promise => { ] : undefined, }); - const pubDateStr: string | undefined = $el.find('div.post-attr').text().split(/\|/)[0]?.trim(); + const pubDateStr: string | undefined = $el.find('div.post-attr').text().split(/\|/, 1)[0]?.trim(); const linkUrl: string | undefined = $aEl.attr('href'); const categoryEls: Element[] = $el.find('span.post-tag a.link-tag').toArray(); const categories: string[] = [...new Set(categoryEls.map((el) => $(el).text()).filter(Boolean))]; @@ -109,7 +109,7 @@ export const handler = async (ctx: Context): Promise => { }); const pubDateStr: string | undefined = $$('div.subject-meta') .text() - ?.split(/发布/)[0]; + ?.split(/发布/, 1)[0]; const categories: string[] = [ ...new Set([ ...(item.category ?? []), diff --git a/lib/routes/duozhuayu/search.tsx b/lib/routes/duozhuayu/search.tsx index 1a977755ba04..0b6a743f4ead 100644 --- a/lib/routes/duozhuayu/search.tsx +++ b/lib/routes/duozhuayu/search.tsx @@ -1,5 +1,5 @@ -/* eslint-disable unicorn/prefer-code-point */ -import aesjs from 'aes-js'; +import crypto from 'node:crypto'; + import { renderToString } from 'hono/jsx/dom/server'; import type { Route } from '@/types'; @@ -29,6 +29,25 @@ export const route: Route = { handler, }; +const generateDeviceId = () => { + const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; + let value = 0; + let bits = 0; + let id = 'b'; + for (const byte of crypto.randomBytes(16)) { + value = (value << 8) | byte; + bits += 8; + while (bits >= 5) { + bits -= 5; + id += alphabet[(value >> bits) & 31]; + } + } + if (bits > 0) { + id += alphabet[(value << (5 - bits)) & 31]; + } + return id; +}; + async function handler(ctx) { const wd = ctx.req.param('wd'); const baseUrl = 'https://www.duozhuayu.com'; @@ -36,14 +55,9 @@ async function handler(ctx) { const link = `${baseUrl}/search/${type}/${wd}`; // token获取见 https://github.com/wong2/userscripts/blob/master/duozhuayu.user.js - const key = [...'DkOliWvFNR7C4WvR'].map((c) => c.charCodeAt()); - const iv = [...'GQWKUE2CVGOOBKXU'].map((c) => c.charCodeAt()); - const aesCfb = new aesjs.ModeOfOperation.cfb(key, iv); - const encrypt = (text) => { - const textBytes = aesjs.utils.utf8.toBytes(text); - const encryptedBytes = aesCfb.encrypt(textBytes); - return aesjs.utils.hex.fromBytes(encryptedBytes); + const cipher = crypto.createCipheriv('aes-128-cfb8', 'DkOliWvFNR7C4WvR', 'GQWKUE2CVGOOBKXU'); + return Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]).toString('hex'); }; const getCustomRequestHeaders = () => { @@ -53,10 +67,13 @@ async function handler(ctx) { const token = encrypt([timestamp, userId, securityKey].join(':')); const requestId = [userId, timestamp, Math.round(1e5 * Math.random())].join('-'); return { - 'x-api-version': '0.0.48', + 'x-api-version': '0.0.85', + 'x-app-platform': 'na', + 'x-app-version': 'na', + 'x-device-id': generateDeviceId(), 'x-refer-request-id': requestId, 'x-request-id': requestId, - 'x-request-misc': '{"platform":"browser","originSource":"search","originFrom":"normal","webVersion":"1.2.201774"}', + 'x-request-misc': '{"platform":"browser","originSource":"search","originFrom":"normal","webVersion":"1.2.525412"}', 'x-request-token': token, 'x-security-key': securityKey, 'x-timestamp': timestamp, @@ -76,7 +93,8 @@ async function handler(ctx) { const item = response.data.data .filter((item) => item.type === type) - .map(({ [type]: item }) => ({ + .map((entry) => entry[type]) + .map((item) => ({ title: item.title, link: `${baseUrl}/books/${item.id}`, pubDate: parseDate(item.updated), // 2023-05-07T13:33:09+08:00 diff --git a/lib/routes/dut/index.ts b/lib/routes/dut/index.ts index 117bf63e663e..870fc93e1891 100644 --- a/lib/routes/dut/index.ts +++ b/lib/routes/dut/index.ts @@ -25,7 +25,7 @@ async function handler(ctx) { let items; let category = ctx.params[1] ?? (Object.hasOwn(defaults, site) ? defaults[site] : ''); - category = Object.hasOwn(shortcuts, site) ? (Object.hasOwn(shortcuts[site], category) ? shortcuts[site][category] : category) : category; + category = Object.hasOwn(shortcuts, site) && Object.hasOwn(shortcuts[site], category) ? shortcuts[site][category] : category; const rootUrl = `https://${site}.dlut.edu.cn`; const currentUrl = `${rootUrl}/${category}.htm`; diff --git a/lib/routes/dytt/index.ts b/lib/routes/dytt/index.ts index d62cccaabbe1..882d20c27233 100644 --- a/lib/routes/dytt/index.ts +++ b/lib/routes/dytt/index.ts @@ -16,7 +16,7 @@ const baseUrl = `https://${domain}`; export const handler = async (ctx: Context): Promise => { const { category = 'gndy/dyzz' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '25', 10); + const limit = Number(ctx.req.query('limit') ?? '25'); const targetUrl: string = new URL(`html/${category.replace(/^html\//, '')}`, baseUrl).href; diff --git a/lib/routes/e-hentai/index.tsx b/lib/routes/e-hentai/index.tsx index a426901f8793..fb5c2073ab74 100644 --- a/lib/routes/e-hentai/index.tsx +++ b/lib/routes/e-hentai/index.tsx @@ -50,7 +50,7 @@ async function handler(ctx) { .toArray() .map((tag) => $(tag).attr('title').replace(/^:/, '')), description: needImages ? '' : ``, - enclosure_url: needTorrents ? (item.find('div.gldown a img[title="Show torrents"]').length > 0 ? item.find('.gldown a').attr('href') : undefined) : undefined, + enclosure_url: needTorrents && item.find('div.gldown a img[title="Show torrents"]').length > 0 ? item.find('.gldown a').attr('href') : undefined, }; }); diff --git a/lib/routes/eastday/24.ts b/lib/routes/eastday/24.ts index 43ed7a699e04..087f48ff9a34 100644 --- a/lib/routes/eastday/24.ts +++ b/lib/routes/eastday/24.ts @@ -92,7 +92,7 @@ async function handler(ctx) { const links = []; for (let i = 2; i <= pageNumber; i++) { - links.push(item.link.replace(/\.html/, `-${i}.html`)); + links.push(item.link.replace(/\.html/, () => `-${i}.html`)); } for (const link of links) { diff --git a/lib/routes/ecnu/art.ts b/lib/routes/ecnu/art.ts index ed057daaac8d..c4afb5bd02c8 100644 --- a/lib/routes/ecnu/art.ts +++ b/lib/routes/ecnu/art.ts @@ -29,7 +29,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('span').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -44,16 +44,15 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); return item; - } else { - // file to download - item.description = '请到原网页访问'; - return item; } + // file to download + item.description = '请到原网页访问'; + return item; }) ) ); diff --git a/lib/routes/ecnu/bksy.ts b/lib/routes/ecnu/bksy.ts index 6b31b948bf44..e08af2e1415d 100644 --- a/lib/routes/ecnu/bksy.ts +++ b/lib/routes/ecnu/bksy.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_date').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('.news_title').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/cee.ts b/lib/routes/ecnu/cee.ts index f32cca255fa5..f1b4530f3ad8 100644 --- a/lib/routes/ecnu/cee.ts +++ b/lib/routes/ecnu/cee.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/chem.ts b/lib/routes/ecnu/chem.ts index 720e02b91c38..53405feb8884 100644 --- a/lib/routes/ecnu/chem.ts +++ b/lib/routes/ecnu/chem.ts @@ -29,7 +29,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.cols_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -44,16 +44,15 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); return item; - } else { - // file to download - item.description = '请到原网页访问'; - return item; } + // file to download + item.description = '请到原网页访问'; + return item; }) ) ); diff --git a/lib/routes/ecnu/chinese.ts b/lib/routes/ecnu/chinese.ts index fbaf0a7fc591..36c379fe88f5 100644 --- a/lib/routes/ecnu/chinese.ts +++ b/lib/routes/ecnu/chinese.ts @@ -29,7 +29,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').attr('title'), })); const items = await Promise.all( @@ -44,16 +44,15 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); return item; - } else { - // file to download - item.description = '请到原网页访问'; - return item; } + // file to download + item.description = '请到原网页访问'; + return item; }) ) ); diff --git a/lib/routes/ecnu/comm.ts b/lib/routes/ecnu/comm.ts index 8c3d3e0d1873..3785a6c98986 100644 --- a/lib/routes/ecnu/comm.ts +++ b/lib/routes/ecnu/comm.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/cs.ts b/lib/routes/ecnu/cs.ts index d17599109acc..e4cbabdb5508 100644 --- a/lib/routes/ecnu/cs.ts +++ b/lib/routes/ecnu/cs.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('span').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/cxcy.ts b/lib/routes/ecnu/cxcy.ts index 0f5ef60fe78e..decc0f4977ac 100644 --- a/lib/routes/ecnu/cxcy.ts +++ b/lib/routes/ecnu/cxcy.ts @@ -50,7 +50,7 @@ export const route: Route = { const filteredEls = $(`div.limit_style1[frag="${fragList[type].frag}"]`).find('table > tbody > tr > td').toArray(); const links = filteredEls.map((el) => ({ pubDate: timezone(parseDate($(el).find('.data').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('.news_title').text(), })); const items = await Promise.all( @@ -65,16 +65,15 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); return item; - } else { - // file to download - item.description = '请到原网页访问'; - return item; } + // file to download + item.description = '请到原网页访问'; + return item; }) ) ); diff --git a/lib/routes/ecnu/dase.ts b/lib/routes/ecnu/dase.ts index b4db79361156..6bb77f0ff81c 100644 --- a/lib/routes/ecnu/dase.ts +++ b/lib/routes/ecnu/dase.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/dx.ts b/lib/routes/ecnu/dx.ts index 45ddc286d073..8c6fe80bf16e 100644 --- a/lib/routes/ecnu/dx.ts +++ b/lib/routes/ecnu/dx.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/dxb.ts b/lib/routes/ecnu/dxb.ts index c7f8c0718290..3bbf32368b6d 100644 --- a/lib/routes/ecnu/dxb.ts +++ b/lib/routes/ecnu/dxb.ts @@ -29,7 +29,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -44,16 +44,15 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); return item; - } else { - // file to download - item.description = '请到原网页访问'; - return item; } + // file to download + item.description = '请到原网页访问'; + return item; }) ) ); diff --git a/lib/routes/ecnu/ed.ts b/lib/routes/ecnu/ed.ts index 0d420244a435..0312caf9fcfb 100644 --- a/lib/routes/ecnu/ed.ts +++ b/lib/routes/ecnu/ed.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/geoai.ts b/lib/routes/ecnu/geoai.ts index e5578ad9835e..c67e30bb96f1 100644 --- a/lib/routes/ecnu/geoai.ts +++ b/lib/routes/ecnu/geoai.ts @@ -29,7 +29,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -44,16 +44,15 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); return item; - } else { - // file to download - item.description = '点击认证后访问内容'; - return item; } + // file to download + item.description = '点击认证后访问内容'; + return item; }) ) ); diff --git a/lib/routes/ecnu/ghcollege.ts b/lib/routes/ecnu/ghcollege.ts index 3deef4413d0c..2271399b12e9 100644 --- a/lib/routes/ecnu/ghcollege.ts +++ b/lib/routes/ecnu/ghcollege.ts @@ -29,7 +29,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.Article_PublishDate').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -44,16 +44,15 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); return item; - } else { - // file to download - item.description = '请至原网页访问内容'; - return item; } + // file to download + item.description = '请至原网页访问内容'; + return item; }) ) ); diff --git a/lib/routes/ecnu/history.ts b/lib/routes/ecnu/history.ts index d9eae7a3df68..a48695bbc14b 100644 --- a/lib/routes/ecnu/history.ts +++ b/lib/routes/ecnu/history.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('span').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/jiaoliu.ts b/lib/routes/ecnu/jiaoliu.ts index 98741a34ae7d..2b3bd4512bfb 100644 --- a/lib/routes/ecnu/jiaoliu.ts +++ b/lib/routes/ecnu/jiaoliu.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('div[style="white-space:nowrap"]').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/jwc.ts b/lib/routes/ecnu/jwc.ts index 56d965a319d6..9b11ae342719 100644 --- a/lib/routes/ecnu/jwc.ts +++ b/lib/routes/ecnu/jwc.ts @@ -33,7 +33,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_date').text()), 8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -43,7 +43,11 @@ export const route: Route = { try { const { data } = await got(item.link); const $ = load(data); - item.description = $('div.article')?.html()?.replaceAll('src="/', `src="${baseUrl}/`)?.replaceAll('href="/', `href="${baseUrl}/`)?.trim(); + item.description = $('div.article') + ?.html() + ?.replaceAll('src="/', () => `src="${baseUrl}/`) + ?.replaceAll('href="/', () => `href="${baseUrl}/`) + ?.trim(); return item; } catch { // intranet diff --git a/lib/routes/ecnu/mks.ts b/lib/routes/ecnu/mks.ts index 891d801d97d7..52226a2b1868 100644 --- a/lib/routes/ecnu/mks.ts +++ b/lib/routes/ecnu/mks.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_date').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('.news_title').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/mxcsy.ts b/lib/routes/ecnu/mxcsy.ts index 1755a8b0401a..14bb6fd346b9 100644 --- a/lib/routes/ecnu/mxcsy.ts +++ b/lib/routes/ecnu/mxcsy.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.Article_PublishDate').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/pharm.ts b/lib/routes/ecnu/pharm.ts index b796b176bf54..b39c14f6dfd2 100644 --- a/lib/routes/ecnu/pharm.ts +++ b/lib/routes/ecnu/pharm.ts @@ -29,7 +29,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -44,16 +44,15 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); return item; - } else { - // file to download - item.description = '请到原网页访问'; - return item; } + // file to download + item.description = '请到原网页访问'; + return item; }) ) ); diff --git a/lib/routes/ecnu/philo.ts b/lib/routes/ecnu/philo.ts index 446532cba1ca..a567c95900ed 100644 --- a/lib/routes/ecnu/philo.ts +++ b/lib/routes/ecnu/philo.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('span').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/phy.ts b/lib/routes/ecnu/phy.ts index a369a99d3d39..e070a50b4de0 100644 --- a/lib/routes/ecnu/phy.ts +++ b/lib/routes/ecnu/phy.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('span').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').attr('title'), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/psy.ts b/lib/routes/ecnu/psy.ts index 6d71ba87e024..2fed52f9c31e 100644 --- a/lib/routes/ecnu/psy.ts +++ b/lib/routes/ecnu/psy.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.Article_PublishDate').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').attr('title'), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/sees.ts b/lib/routes/ecnu/sees.ts index 1c5cd682f465..429c71dec9a4 100644 --- a/lib/routes/ecnu/sees.ts +++ b/lib/routes/ecnu/sees.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/sei.ts b/lib/routes/ecnu/sei.ts index 2040bd15a025..26973570a924 100644 --- a/lib/routes/ecnu/sei.ts +++ b/lib/routes/ecnu/sei.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.data-list-time').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/spm.ts b/lib/routes/ecnu/spm.ts index e83815416737..8e1e7f73dd96 100644 --- a/lib/routes/ecnu/spm.ts +++ b/lib/routes/ecnu/spm.ts @@ -29,7 +29,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -44,16 +44,15 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); return item; - } else { - // file to download - item.description = '请到原网页访问'; - return item; } + // file to download + item.description = '请到原网页访问'; + return item; }) ) ); diff --git a/lib/routes/ecnu/stat.ts b/lib/routes/ecnu/stat.ts index 634568960b8b..15f0e70908f8 100644 --- a/lib/routes/ecnu/stat.ts +++ b/lib/routes/ecnu/stat.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').attr('title'), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/ecnu/tyxx.ts b/lib/routes/ecnu/tyxx.ts index 7ae1adffaa11..a257b7ada49a 100644 --- a/lib/routes/ecnu/tyxx.ts +++ b/lib/routes/ecnu/tyxx.ts @@ -27,7 +27,7 @@ export const route: Route = { .toArray() .map((el) => ({ pubDate: timezone(parseDate($(el).find('.news_meta').text()), +8), - link: new URL($(el).find('a').attr('href'), baseUrl).toString(), + link: new URL($(el).find('a').attr('href'), baseUrl).href, title: $(el).find('a').text(), })); const items = await Promise.all( @@ -41,7 +41,7 @@ export const route: Route = { const attr = el.tagName === 'img' ? 'src' : 'href'; const val = $el.attr(attr); if (val) { - $el.attr(attr, new URL(val, baseUrl).toString()); + $el.attr(attr, new URL(val, baseUrl).href); } }); item.description = $read.html()?.trim(); diff --git a/lib/routes/economist/full.ts b/lib/routes/economist/full.ts index 657b0f5a281b..814ce9e2cd2e 100644 --- a/lib/routes/economist/full.ts +++ b/lib/routes/economist/full.ts @@ -49,8 +49,9 @@ async function handler(ctx) { const endpoint = ctx.req.param('endpoint'); const feed = await parser.parseURL(`https://www.economist.com/${endpoint}/rss.xml`); + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 30; const items = await Promise.all( - feed.items.slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 30).map(async (item) => { + feed.items.slice(0, limit).map(async (item) => { const path = item.link.slice(item.link.lastIndexOf('/') + 1); const isNotCollection = !/^\d{4}-\d{2}-\d{2}$/.test(path); const itemDetails = isNotCollection ? await getArticleDetail(item.link) : null; diff --git a/lib/routes/economist/global-business-review.ts b/lib/routes/economist/global-business-review.ts index 8dff9446e7e7..1cd0bd0be7a7 100644 --- a/lib/routes/economist/global-business-review.ts +++ b/lib/routes/economist/global-business-review.ts @@ -104,8 +104,9 @@ async function handler(ctx) { url: 'https://api.hummingbird.businessreview.global/api/toc/get_articles', }); + const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10; const items = await Promise.all( - response.data.articles.new.slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 10).map(async (item) => ({ + response.data.articles.new.slice(0, limit).map(async (item) => ({ title: parseTitle(item.body.title, [main_language]), description: await getArticleDetail(item.article_id, language), category: parseTitle(item.body.fly_title, [main_language]), diff --git a/lib/routes/eeo/kuaixun.ts b/lib/routes/eeo/kuaixun.ts index 28cf67b811ba..620a3e9707c1 100644 --- a/lib/routes/eeo/kuaixun.ts +++ b/lib/routes/eeo/kuaixun.ts @@ -12,7 +12,7 @@ import timezone from '@/utils/timezone'; import { renderDescription } from './templates/description'; export const handler = async (ctx: Context): Promise => { - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '50', 10); + const limit = Number(ctx.req.query('limit') ?? '50'); const baseUrl = 'https://www.eeo.com.cn'; const apiUrl = 'https://app.eeo.com.cn'; diff --git a/lib/routes/efe/index.ts b/lib/routes/efe/index.ts new file mode 100644 index 000000000000..73ffeb9589f0 --- /dev/null +++ b/lib/routes/efe/index.ts @@ -0,0 +1,97 @@ +import { load } from 'cheerio'; +import pMap from 'p-map'; + +import type { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +const rootUrl = 'https://efe.com'; + +const categories: Record = { + mundo: 'Mundo', + espana: 'España', + economia: 'Economía', + cultura: 'Cultura', + 'ciencia-y-tecnologia': 'Ciencia y Tecnología', + deportes: 'Deportes', + salud: 'Salud', + 'medio-ambiente': 'Medio Ambiente', + educacion: 'Educación', + 'euro-efe': 'EuroEFE', +}; + +export const route: Route = { + path: '/:category?', + name: 'Category', + maintainers: ['mlkgrnt'], + example: '/efe/mundo', + parameters: { + category: { + description: 'Category slug, see table below. Defaults to mundo.', + default: 'mundo', + }, + }, + handler, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['efe.com/:category'], + target: '/:category', + }, + ], +}; + +async function handler(ctx) { + const category = ctx.req.param('category') || 'mundo'; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 20; + const pageUrl = `${rootUrl}/${category}/`; + + const response = await ofetch(pageUrl); + const $ = load(response); + + const links = $('.e-loop-item .elementor-widget-theme-post-title a[href]') + .toArray() + .map((el) => $(el).attr('href')) + .filter((href): href is string => !!href && href.startsWith(`${rootUrl}/${category}/`) && /\/\d{4}-\d{2}-\d{2}\//.test(href)); + + const items = await pMap( + links.slice(0, limit), + (link) => + cache.tryGet(link, async () => { + const detail = await ofetch(link); + const $detail = load(detail); + + const title = $detail('title').text(); + const dateMatch = detail.match(/"datePublished":\s*"([^"]+)"/); + const pubDate = dateMatch ? parseDate(dateMatch[1]) : undefined; + + const image = $detail('meta[property="og:image"]').attr('content'); + const content = $detail('.elementor-widget-theme-post-content'); + content.find('.auto-banner').remove(); + const description = (image ? `
` : '') + (content.html() || ''); + + return { + title, + link, + pubDate, + description, + }; + }), + { concurrency: 2 } + ); + + return { + title: `EFE Noticias - ${categories[category] || category}`, + link: pageUrl, + item: items, + }; +} diff --git a/lib/routes/efe/namespace.ts b/lib/routes/efe/namespace.ts new file mode 100644 index 000000000000..55dcbb41ebee --- /dev/null +++ b/lib/routes/efe/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'EFE Noticias', + url: 'efe.com', + lang: 'es', +}; diff --git a/lib/routes/ehentai/ehapi.ts b/lib/routes/ehentai/ehapi.ts index ba9284beaa7c..add4708542f1 100644 --- a/lib/routes/ehentai/ehapi.ts +++ b/lib/routes/ehentai/ehapi.ts @@ -161,7 +161,7 @@ function getBittorrent(cache, bittorrent_page_url) { const match = onclick.match(/'(.*?)'/); if (match) { bittorrent_url = match[1]; - const match_p = bittorrent_url.match(/torrent\?p=(.*?)$/); + const match_p = bittorrent_url.match(/torrent\?p=(.*)$/); if (match_p) { p = match_p[1]; } @@ -179,7 +179,7 @@ function updateBittorrent_url(cache, items) { // 下种子文件需要动态密码,密码每几次请求就更新一次 for (const item of items) { if (item.enclosure_url) { - item.enclosure_url = item.enclosure_url.replace(/torrent\?p=.*$/, `torrent?p=${p}`); + item.enclosure_url = item.enclosure_url.replace(/torrent\?p=.*$/, () => `torrent?p=${p}`); cache.set(item.bittorrent_page_url, item.enclosure_url); } } @@ -193,13 +193,12 @@ async function gatherItemsByPage(cache, url, get_bittorrent = false, embed_thumb } async function getFavoritesItems(cache, favcat, inline_set, page, get_bittorrent = false, embed_thumb = false) { - const response = await ehgot(`favorites.php?favcat=${favcat}&inline_set=${inline_set}`); if (page) { return gatherItemsByPage(cache, `favorites.php?favcat=${favcat}&next=${page}`, get_bittorrent, embed_thumb); - } else { - const items = await parsePage(cache, response.data, get_bittorrent, embed_thumb); - return updateBittorrent_url(cache, items); } + const response = await ehgot(`favorites.php?favcat=${favcat}&inline_set=${inline_set}`); + const items = await parsePage(cache, response.data, get_bittorrent, embed_thumb); + return updateBittorrent_url(cache, items); } function getSearchItems(cache, params, page, get_bittorrent = false, embed_thumb = false) { diff --git a/lib/routes/elamigos/index.ts b/lib/routes/elamigos/index.ts index faa942b0a13d..6333a1d847eb 100644 --- a/lib/routes/elamigos/index.ts +++ b/lib/routes/elamigos/index.ts @@ -117,12 +117,13 @@ function sanitizeHtml(pageHtml: string): string { $page('script, style, link, nav').remove(); $page('*').each((_: number, elem: any) => { - if (elem.attribs) { - const attributes = Object.keys(elem.attribs); - for (const attr of attributes) { - if (attr.toLowerCase().startsWith('on')) { - $page(elem).removeAttr(attr); - } + if (!elem.attribs) { + return; + } + const attributes = Object.keys(elem.attribs); + for (const attr of attributes) { + if (attr.toLowerCase().startsWith('on')) { + $page(elem).removeAttr(attr); } } }); diff --git a/lib/routes/eshukan/academic.ts b/lib/routes/eshukan/academic.ts index b9771dbd5182..d88be34fc42d 100644 --- a/lib/routes/eshukan/academic.ts +++ b/lib/routes/eshukan/academic.ts @@ -10,7 +10,7 @@ import { renderDescription } from './templates/description'; export const handler = async (ctx) => { const { id = '1' } = ctx.req.param(); - const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10; + const limit = ctx.req.query('limit') ? Number(ctx.req.query('limit')) : 10; const rootUrl = 'https://www.eshukan.com'; const currentUrl = new URL(`academic/index.aspx?cid=${id}`, rootUrl).href; diff --git a/lib/routes/esquirehk/tag.tsx b/lib/routes/esquirehk/tag.tsx index 21dcda81de0b..e15384ec2d66 100644 --- a/lib/routes/esquirehk/tag.tsx +++ b/lib/routes/esquirehk/tag.tsx @@ -96,7 +96,7 @@ const renderSubpages = (subpages): string => break; } case 'video_block': { - const videoId = page.source?.split('&')[0]; + const videoId = page.source?.split('&', 1)[0]; blocks.push( `; case 'leading': case 'img': return `
`${key}="${node.attribs[key]}"`) + ? Object.entries(node.attribs) + .map(([key, value]) => `${key}="${value}"`) .join(' ') : `url="${node.url}"` }>
${ diff --git a/lib/routes/scnu/physics-school-announcements-and-news.ts b/lib/routes/scnu/physics-school-announcements-and-news.ts index aa82dcef0386..aba172628686 100644 --- a/lib/routes/scnu/physics-school-announcements-and-news.ts +++ b/lib/routes/scnu/physics-school-announcements-and-news.ts @@ -39,7 +39,7 @@ const getAritlces = async (category, url, cache) => { }; const getItemsFromURLs = async (URLs, cache) => { - let items = Object.keys(URLs).map((key) => getAritlces(key, URLs[key], cache)); + let items = Object.entries(URLs).map(([key, value]) => getAritlces(key, value, cache)); items = await Promise.all(items); items = items.flat(); return items; diff --git a/lib/routes/scoop/apps.tsx b/lib/routes/scoop/apps.tsx index 5e68afbe61a3..b15d694b0c01 100644 --- a/lib/routes/scoop/apps.tsx +++ b/lib/routes/scoop/apps.tsx @@ -34,7 +34,7 @@ const filters = { export const handler = async (ctx: Context): Promise => { const { query = 's=2&d=1&n=true&dm=true&o=true' } = ctx.req.param(); - const limit: number = Number.parseInt(ctx.req.query('limit') ?? '50', 10); + const limit = Number(ctx.req.query('limit') ?? '50'); const baseUrl = 'https://scoop.sh'; const apiBaseUrl = 'https://scoopsearch.search.windows.net'; diff --git a/lib/routes/scut/jwc/news.ts b/lib/routes/scut/jwc/news.ts index 62f4729038a9..a5d5c68b5d60 100644 --- a/lib/routes/scut/jwc/news.ts +++ b/lib/routes/scut/jwc/news.ts @@ -33,7 +33,7 @@ const generateArticlePubDate = (createDateStr) => { const isRedirectPage = (data) => !!data.link; -const resolveRelativeUrl = (html) => html.replaceAll('src="/', `src="${new URL('.', baseUrl).href}`).replaceAll('href="/', `href="${new URL('.', baseUrl).href}`); +const resolveRelativeUrl = (html) => html.replaceAll('src="/', () => `src="${new URL('.', baseUrl).href}`).replaceAll('href="/', () => `href="${new URL('.', baseUrl).href}`); const apiSuccessAssert = (data) => { if (!data.success) { diff --git a/lib/routes/scut/jwc/notice.ts b/lib/routes/scut/jwc/notice.ts index f826a3fe5118..3d2404ea616c 100644 --- a/lib/routes/scut/jwc/notice.ts +++ b/lib/routes/scut/jwc/notice.ts @@ -43,7 +43,7 @@ const generateArticlePubDate = (createDateStr) => { const isRedirectPage = (data) => !!data.link; -const resolveRelativeUrl = (html) => html.replaceAll('src="/', `src="${new URL('.', baseUrl).href}`).replaceAll('href="/', `href="${new URL('.', baseUrl).href}`); +const resolveRelativeUrl = (html) => html.replaceAll('src="/', () => `src="${new URL('.', baseUrl).href}`).replaceAll('href="/', () => `href="${new URL('.', baseUrl).href}`); const apiSuccessAssert = (data) => { if (!data.success) { diff --git a/lib/routes/scut/jwc/school.ts b/lib/routes/scut/jwc/school.ts index f10b34b4aa93..0d7823d38a7f 100644 --- a/lib/routes/scut/jwc/school.ts +++ b/lib/routes/scut/jwc/school.ts @@ -40,7 +40,7 @@ const generateArticlePubDate = (createDateStr) => { const isRedirectPage = (data) => !!data.link; -const resolveRelativeUrl = (html) => html.replaceAll('src="/', `src="${new URL('.', baseUrl).href}`).replaceAll('href="/', `href="${new URL('.', baseUrl).href}`); +const resolveRelativeUrl = (html) => html.replaceAll('src="/', () => `src="${new URL('.', baseUrl).href}`).replaceAll('href="/', () => `href="${new URL('.', baseUrl).href}`); const apiSuccessAssert = (data) => { if (!data.success) { diff --git a/lib/routes/sdmuseum/exhibitions.tsx b/lib/routes/sdmuseum/exhibitions.tsx new file mode 100644 index 000000000000..c89178e63425 --- /dev/null +++ b/lib/routes/sdmuseum/exhibitions.tsx @@ -0,0 +1,162 @@ +import { load } from 'cheerio'; +import { renderToString } from 'hono/jsx/dom/server'; + +import type { DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +import { namespace } from './namespace'; + +// format the date to YYYY-MM-DD and handle missing year or month +const extractDates = (durationStr: string) => { + let startDate: string | undefined; + let endDate: string | undefined; + + if (!durationStr) { + return { startDate, endDate }; + } + + const parts = durationStr.split(/[-—~]+/).map((p) => p.trim()); + const startStr = parts[0]; + const endStr = parts[1]; + + let startYear: string | undefined; + let startMonth: string | undefined; + + const startRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/; + const startMatch = startStr.match(startRegex); + + if (startMatch) { + startYear = startMatch[1]; + startMonth = startMatch[2].padStart(2, '0'); + const startDay = startMatch[3].padStart(2, '0'); + startDate = `${startYear}-${startMonth}-${startDay}`; + } + + if (endStr && startDate) { + const endRegex = /(?:(\d{4})年)?(?:(\d{1,2})月)?(\d{1,2})日/; + const endMatch = endStr.match(endRegex); + + if (endMatch) { + const matchYear = endMatch[1]; + const matchMonth = endMatch[2]?.padStart(2, '0'); + const matchDay = endMatch[3].padStart(2, '0'); + + const finalEndYear = matchYear || startYear; + const finalEndMonth = matchMonth || startMonth; + const finalEndDay = matchDay; + endDate = `${finalEndYear}-${finalEndMonth}-${finalEndDay}`; + } + } + + return { startDate, endDate }; +}; + +export const route: Route = { + path: '/exhibitions', + categories: ['travel'], + example: '/sdmuseum/exhibitions', + parameters: {}, + name: 'Temporary Exhibitions', + maintainers: ['magazian'], + radar: [ + { + source: ['www.sdmuseum.com/col/col271702/index.html'], + target: '/exhibitions', + }, + ], + + handler: async () => { + const baseUrl = 'https://www.sdmuseum.com'; + const apiUrl = `${baseUrl}/col/col271702/index.html`; + const museumName = namespace.zh?.name || namespace.name; + + const response = await got({ + method: 'get', + url: apiUrl, + }); + + const $ = load(response.data, { + xml: true, // use xmlMode to preserve CDATA sections inside