Torrent download statistics
+-
+ {{each numStats}}
+
- {{$value.percent}} {{$value.desc}} + {{/each}} +
Table View
+ {{if tableData}} +Category | Count | Percent |
---|---|---|
{{$value.key}} | +{{$value.count}} | +{{$value.percent}} | +
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a7c9da72d0ef0b..77026d75ece61f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ // https://github.com/devcontainers/images/blob/v0.3.24/src/javascript-node/.devcontainer/devcontainer.json { "name": "Node.js", - "image": "mcr.microsoft.com/devcontainers/javascript-node:20-bookworm", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22-bookworm", // Configure tool-specific properties. "customizations": { @@ -16,9 +16,9 @@ "EditorConfig.EditorConfig", "esbenp.prettier-vscode", "deepscan.vscode-deepscan", - "rangav.vscode-thunder-client", "SonarSource.sonarlint-vscode", "unifiedjs.vscode-mdx", + "VASubasRaj.flashpost", // Thunder Client is paywalled in WSL/Codespaces/SSH > 2.30.0 "ZihanLi.at-helper" ] } @@ -38,9 +38,9 @@ } }, - "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 && 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 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/*", - "updateContentCommand": "pnpm i && pnpm rb", + "updateContentCommand": "export JAVA_HOME=/usr/lib/jvm/default-java && pnpm config set store-dir ~/.local/share/pnpm/store && pnpm i && pnpm rb", // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "pnpm i && pnpm rb", diff --git a/.dockerignore b/.dockerignore index 1b633a2d9b21af..81d65ee79f4eb1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -25,12 +25,14 @@ test .(yarn|npm|nvm)rc *.md app.json +eslint.config.mjs docker-compose* fly.toml jsconfig.json npm-debug.log process.json package-lock.json +vitest.config.ts vercel.json # git but keep the git commit hash diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index c0bb49c95c8c24..00000000000000 --- a/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -coverage -.vscode -docker-compose.yml -!/.github -lib/routes-deprecated -lib/router.js -babel.config.js -scripts/docker/minify-docker.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 4bfca3e1dcb3ad..00000000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "extends": ["eslint:recommended", "plugin:n/recommended", "plugin:unicorn/recommended", "plugin:prettier/recommended", "plugin:yml/recommended", "plugin:@typescript-eslint/recommended"], - "parser": "@typescript-eslint/parser", - "root": true, - "plugins": ["prettier", "@stylistic", "unicorn", "@typescript-eslint"], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "env": { - "node": true, - "es2024": true, - "browser": true - }, - "rules": { - // possible problems - "array-callback-return": ["error", { "allowImplicit": true }], - "no-await-in-loop": 2, - "no-control-regex": 0, - "no-duplicate-imports": 2, - "no-prototype-builtins": 0, - // suggestions - "arrow-body-style": 2, - "block-scoped-var": 2, - "curly": 2, - "dot-notation": 2, - "eqeqeq": 2, - "default-case": ["warn", { "commentPattern": "^no default$" }], - "default-case-last": 2, - "no-console": 2, - "no-eval": 2, - "no-extend-native": 2, - "no-extra-label": 2, - "no-implicit-coercion": ["error", { "boolean": false, "number": false, "string": false, "disallowTemplateShorthand": true }], - "no-implicit-globals": 2, - "no-labels": 2, - "no-multi-str": 2, - "no-new-func": 2, - "no-restricted-imports": 2, - "no-unneeded-ternary": 2, - "no-useless-computed-key": 2, - "no-useless-concat": 1, - "no-useless-rename": 2, - "no-var": 2, - "object-shorthand": 2, - "prefer-arrow-callback": 2, - "prefer-const": 2, - "prefer-object-has-own": 2, - "no-useless-escape": 1, - "prefer-regex-literals": ["error", { "disallowRedundantWrapping": true }], - "require-await": 2, - // typescript - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-var-requires": 0, - // plugin specific - "unicorn/consistent-destructuring": 1, - "unicorn/consistent-function-scoping": 1, - "unicorn/explicit-length-check": 0, - "unicorn/filename-case": ["error", { "case": "kebabCase", "ignore": [".*\\.(yaml|yml)$", "RequestInProgress\\.js$"] }], - "unicorn/new-for-builtins": 0, - "unicorn/no-array-callback-reference": 1, - "unicorn/no-array-reduce": 1, - "unicorn/no-await-expression-member": 0, - "unicorn/no-empty-file": 1, - "unicorn/no-hex-escape": 1, - "unicorn/no-null": 0, - "unicorn/no-object-as-default-parameter": 1, - "unicorn/no-process-exit": 0, - "unicorn/no-useless-switch-case": 0, - "unicorn/no-useless-undefined": ["error", { "checkArguments": false }], - "unicorn/numeric-separators-style": [ - "warn", - { - "onlyIfContainsSeparator": false, - "number": { "minimumDigits": 7, "groupLength": 3 }, - "binary": { "minimumDigits": 9, "groupLength": 4 }, - "octal": { "minimumDigits": 9, "groupLength": 4 }, - "hexadecimal": { "minimumDigits": 5, "groupLength": 2 } - } - ], - "unicorn/prefer-code-point": 1, - "unicorn/prefer-logical-operator-over-ternary": 1, - "unicorn/prefer-module": 0, - "unicorn/prefer-node-protocol": 0, - "unicorn/prefer-number-properties": ["warn", { "checkInfinity": false }], - "unicorn/prefer-object-from-entries": 1, - "unicorn/prefer-regexp-test": 1, - "unicorn/prefer-spread": 1, - "unicorn/prefer-string-replace-all": 1, - "unicorn/prefer-string-slice": 0, - "unicorn/prefer-switch": ["warn", { "emptyDefaultCase": "do-nothing-comment" }], - "unicorn/prefer-top-level-await": 0, - "unicorn/prevent-abbreviations": 0, - "unicorn/switch-case-braces": ["error", "avoid"], - "unicorn/text-encoding-identifier-case": 0, - // previous eslint formatting rules - "@stylistic/arrow-parens": 2, - "@stylistic/arrow-spacing": 2, - "@stylistic/comma-spacing": 2, - "@stylistic/comma-style": 2, - "@stylistic/function-call-spacing": 2, - "@stylistic/keyword-spacing": 2, - "@stylistic/linebreak-style": 2, - "@stylistic/lines-around-comment": ["error", { "beforeBlockComment": false }], - "@stylistic/no-multiple-empty-lines": 2, - "@stylistic/no-trailing-spaces": 2, - "@stylistic/rest-spread-spacing": 2, - "@stylistic/semi": 2, - "@stylistic/space-before-blocks": 2, - "@stylistic/space-in-parens": 2, - "@stylistic/space-infix-ops": 2, - "@stylistic/space-unary-ops": 2, - "@stylistic/spaced-comment": 2, - // https://github.com/eslint-community/eslint-plugin-n - "n/no-extraneous-require": ["error", { "allowModules": ["puppeteer-extra-plugin-user-preferences", "puppeteer-extra-plugin-user-data-dir"] }], - "n/no-deprecated-api": 1, - "n/no-missing-import": 0, - "n/no-missing-require": 0, - "n/no-process-exit": 0, - "n/no-unpublished-import": 0, - "n/no-unpublished-require": ["error", { "allowModules": ["tosource"] }], - "prettier/prettier": 0, - "yml/quotes": ["error", { "prefer": "single" }], - "yml/no-empty-mapping-value": 0 - }, - "overrides": [ - { - "files": ["*.yaml", "*.yml"], - "parser": "yaml-eslint-parser", - "rules": { - "lines-around-comment": ["error", { "beforeBlockComment": false }] - } - } - ] -} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 1a29f35400dfee..00000000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,5 +0,0 @@ -# These are supported funding model platforms -github: DIYgod -patreon: DIYgod -open_collective: RSSHub -custom: ['https://afdian.net/a/diygod', 'https://docs.rsshub.app/sponsor'] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 845d7d382e9586..91cee7d772ea15 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,19 +4,40 @@ updates: directory: '/' schedule: interval: daily - time: '21:00' - open-pull-requests-limit: 10 + time: '08:00' + open-pull-requests-limit: 100 labels: - 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 + # 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 + - dependency-name: art-template + versions: ['>=4.13.3'] + # no longer includes KJUR.crypto.Cipher for RSA - dependency-name: jsrsasign - versions: ['>=11.0.0'] # no longer includes KJUR.crypto.Cipher for RSA + versions: ['>=11.0.0'] + groups: + eslint: + patterns: + - 'eslint' + - '@eslint/*' + opentelemetry: + patterns: + - '@opentelemetry/*' + typescript-eslint: + patterns: + - '@typescript-eslint/*' - package-ecosystem: 'github-actions' directory: '/' schedule: interval: daily - time: '21:00' - open-pull-requests-limit: 10 + time: '08:00' + open-pull-requests-limit: 100 labels: - dependencies diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml index 2665bca08803f6..9043b0c849f274 100644 --- a/.github/workflows/build-assets.yml +++ b/.github/workflows/build-assets.yml @@ -8,25 +8,20 @@ on: paths: - 'lib/**/*.ts' -permissions: - contents: read - jobs: build: runs-on: ubuntu-latest name: Build assets - timeout-minutes: 60 + timeout-minutes: 5 permissions: contents: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install pnpm - uses: pnpm/action-setup@v3 - with: - version: 8 + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Use Node.js Active LTS - uses: actions/setup-node@v4 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: lts/* cache: 'pnpm' @@ -35,7 +30,7 @@ jobs: - name: Build assets run: pnpm build - name: Deploy - uses: peaceiris/actions-gh-pages@v4 + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./assets @@ -45,19 +40,27 @@ jobs: keep_files: true - name: Build docs run: pnpm build:docs + - id: check-env + env: + DOCS_API_TOKEN: ${{ secrets.DOCS_API_TOKEN }} + if: ${{ env.DOCS_API_TOKEN != '' }} + run: echo "defined=true" >> $GITHUB_OUTPUT - name: Checkout docs - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + if: steps.check-env.outputs.defined == 'true' with: repository: 'RSSNext/rsshub-docs' token: ${{ secrets.DOCS_API_TOKEN }} path: rsshub-docs - name: Update docs + if: steps.check-env.outputs.defined == 'true' run: | cp -r ./assets/build/docs/en/* ./rsshub-docs/src/routes cp -r ./assets/build/docs/zh/* ./rsshub-docs/src/zh/routes cp ./lib/types.ts ./rsshub-docs/.vitepress/theme/types.ts cp ./scripts/workflow/data.ts ./rsshub-docs/.vitepress/config/data.ts - name: Commit docs + if: steps.check-env.outputs.defined == 'true' run: | cd rsshub-docs git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7985f4e1501b46..02dc48f166d8bd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -48,11 +48,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,7 +65,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -78,6 +78,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/comment-on-issue.yml b/.github/workflows/comment-on-issue.yml index 9ef4dd5619cc74..f4d21656b4919a 100644 --- a/.github/workflows/comment-on-issue.yml +++ b/.github/workflows/comment-on-issue.yml @@ -9,20 +9,20 @@ jobs: name: Call maintainers runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + issues: write if: github.event.sender.login != 'issuehunt-oss[bot]' steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 8 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: lts/* cache: 'pnpm' - name: Install dependencies (pnpm) # import remark-parse and unified run: pnpm i - name: Generate feedback - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/dependabot-fork.yml b/.github/workflows/dependabot-fork.yml index b39ec2ae7cd20f..8cca92385a1172 100644 --- a/.github/workflows/dependabot-fork.yml +++ b/.github/workflows/dependabot-fork.yml @@ -10,10 +10,10 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Comment Dependabot PR - uses: thollander/actions-comment-pull-request@v2 + uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1 with: message: '@dependabot ignore this dependency' GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index c22edaa347b7c0..1cbb2a68c48ec1 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -29,33 +29,32 @@ jobs: runs-on: ubuntu-latest needs: check-env if: needs.check-env.outputs.check-docker == 'true' - timeout-minutes: 120 + timeout-minutes: 60 permissions: packages: write - contents: read id-token: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install cosign if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@v3 + uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the Container registry - uses: docker/login-action@v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -63,7 +62,7 @@ jobs: - name: Extract Docker metadata (ordinary version) id: meta-ordinary - uses: docker/metadata-action@v5 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: | ${{ secrets.DOCKER_USERNAME }}/rsshub @@ -76,7 +75,7 @@ jobs: - name: Build and push Docker image (ordinary version) id: build-and-push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 with: context: . push: true @@ -94,7 +93,7 @@ jobs: - name: Extract Docker metadata (Chromium-bundled version) id: meta-chromium-bundled - uses: docker/metadata-action@v5 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: | ${{ secrets.DOCKER_USERNAME }}/rsshub @@ -107,7 +106,7 @@ jobs: - name: Build and push Docker image (Chromium-bundled version) id: build-and-push-chromium - uses: docker/build-push-action@v5 + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 with: context: . build-args: PUPPETEER_SKIP_DOWNLOAD=0 @@ -131,10 +130,10 @@ jobs: if: needs.check-env.outputs.check-docker == 'true' timeout-minutes: 5 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Docker Hub Description - uses: peter-evans/dockerhub-description@v4 + uses: peter-evans/dockerhub-description@0505d8b04853a30189aee66f5bb7fd1511bbac71 # v4.0.1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/docker-test-cont.yml b/.github/workflows/docker-test-cont.yml index bb13948ab4b75a..b173f35b3bfb7c 100644 --- a/.github/workflows/docker-test-cont.yml +++ b/.github/workflows/docker-test-cont.yml @@ -9,20 +9,22 @@ jobs: testRoute: name: Route test runs-on: ubuntu-latest + permissions: + pull-requests: write if: ${{ github.event.workflow_run.conclusion == 'success' }} # skip if unsuccessful steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # https://github.com/orgs/community/discussions/25220 - name: Search the PR that triggered this workflow - uses: potiuk/get-workflow-origin@v1_5 + uses: potiuk/get-workflow-origin@e2dae063368361e4cd1f510e8785cd73bca9352e # v1_5 id: source-run-info with: token: ${{ secrets.GITHUB_TOKEN }} sourceRunId: ${{ github.event.workflow_run.id }} - name: Fetch PR data via GitHub API - uses: octokit/request-action@v2.x + uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0 id: pr-data with: route: GET /repos/{repo}/pulls/{number} @@ -33,7 +35,7 @@ jobs: - name: Fetch affected routes id: fetch-route - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 env: PULL_REQUEST: ${{ steps.pr-data.outputs.data }} with: @@ -49,16 +51,19 @@ jobs: - name: Fetch Docker image if: (env.TEST_CONTINUE) - uses: dawidd6/action-download-artifact@v3 + uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 # v9 with: workflow: ${{ github.event.workflow_run.workflow_id }} run_id: ${{ github.event.workflow_run.id }} + name: docker-image + path: ../artifacts-${{ github.run_id }} - name: Import Docker image and set up Docker container if: (env.TEST_CONTINUE) + working-directory: ../artifacts-${{ github.run_id }} run: | set -ex - zstd -d --stdout docker-image/rsshub.tar.zst | docker load + zstd -d --stdout rsshub.tar.zst | docker load docker run -d \ --name rsshub \ -e NODE_ENV=dev \ @@ -68,11 +73,9 @@ jobs: -p 1200:1200 \ rsshub:latest - - uses: pnpm/action-setup@v3 - with: - version: 8 + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 if: (env.TEST_CONTINUE) with: node-version: lts/* @@ -86,7 +89,7 @@ jobs: if: (env.TEST_CONTINUE) id: generate-feedback timeout-minutes: 10 - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 env: TEST_BASEURL: http://localhost:1200 TEST_ROUTES: ${{ steps.fetch-route.outputs.result }} @@ -104,7 +107,7 @@ jobs: - name: Pull Request Labeler if: ${{ failure() }} - uses: actions-cool/issues-helper@v3 + uses: actions-cool/issues-helper@a610082f8ac0cf03e357eb8dd0d5e2ba075e017e # v3.6.0 with: actions: 'add-labels' token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 2f3031e84921a5..2b871267406f31 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -13,10 +13,6 @@ on: types: [opened, reopened, synchronize, edited] # Please, always create a pull request instead of push to master. -permissions: - contents: read - pull-requests: write - concurrency: group: docker-test-${{ github.ref_name }} cancel-in-progress: true @@ -24,24 +20,27 @@ concurrency: jobs: test: name: Docker build & tests + permissions: + pull-requests: write + attestations: write runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Docker Buildx # needed by `cache-from` - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: rsshub flavor: latest=true - name: Build Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 with: context: . build-args: PUPPETEER_SKIP_DOWNLOAD=0 # also test bundling Chromium @@ -55,7 +54,7 @@ jobs: - name: Pull Request Labeler if: ${{ failure() }} - uses: actions-cool/issues-helper@v3 + uses: actions-cool/issues-helper@a610082f8ac0cf03e357eb8dd0d5e2ba075e017e # v3.6.0 with: actions: 'add-labels' token: ${{ secrets.GITHUB_TOKEN }} @@ -69,7 +68,7 @@ jobs: run: docker save rsshub:latest | zstdmt -o rsshub.tar.zst - name: Upload Docker image - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: docker-image path: rsshub.tar.zst diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 705b8281c27e61..53ffc14f573d66 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -5,9 +5,6 @@ on: branches: - master -permissions: - contents: read - jobs: format: permissions: @@ -17,11 +14,9 @@ jobs: timeout-minutes: 5 steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 8 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: lts/* cache: 'pnpm' diff --git a/.github/workflows/issue-command.yml b/.github/workflows/issue-command.yml index 18d84c591cf1cf..12681ce3df2b02 100644 --- a/.github/workflows/issue-command.yml +++ b/.github/workflows/issue-command.yml @@ -4,9 +4,6 @@ on: issue_comment: types: [created] -permissions: - contents: read - jobs: rebase: name: Automatic Rebase @@ -18,13 +15,13 @@ jobs: pull-requests: write steps: - name: Checkout the latest code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Automatic Rebase uses: cirrus-actions/rebase@1.8 env: - GITHUB_TOKEN: ${{ secrets.TOKEN_SUPER }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} self-assign: name: Self Assign @@ -32,10 +29,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 permissions: - contents: read issues: write steps: - - uses: bdougie/take-action@v1.6.1 + - uses: bdougie/take-action@1439165ac45a7461c2d89a59952cd7d941964b87 # v1.6.1 with: token: ${{ secrets.GITHUB_TOKEN }} trigger: '/wip' @@ -46,19 +42,36 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 permissions: - contents: read + attestations: write issues: write + pull-requests: write steps: + - name: Fetch PR data (for PR) + if: github.event.issue.pull_request + uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0 + id: pr-data + with: + route: GET /repos/{repo}/pulls/{number} + repo: ${{ github.repository }} + number: ${{ github.event.issue.number }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout - uses: actions/checkout@v4 + if: ${{ !github.event.issue.pull_request }} + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Install pnpm - uses: pnpm/action-setup@v3 + - name: Checkout PR + if: github.event.issue.pull_request + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - version: 8 + ref: ${{ fromJson(steps.pr-data.outputs.data).head.ref }} + + - name: Install pnpm + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Use Node.js Active LTS - uses: actions/setup-node@v4 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: lts/* cache: 'pnpm' @@ -68,7 +81,7 @@ jobs: - name: Fetch affected routes id: fetch-route - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 env: EVENT: ${{ toJson(github.event) }} with: @@ -95,7 +108,7 @@ jobs: - name: Generate feedback if: env.TEST_CONTINUE - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 env: TEST_BASEURL: http://localhost:1200 TEST_ROUTES: ${{ steps.fetch-route.outputs.result }} @@ -111,11 +124,11 @@ jobs: await test({ github, context, core }, link, routes, number) - name: Print logs - if: (env.TEST_CONTINUE) + if: env.TEST_CONTINUE run: cat ${{ github.workspace }}/logs/combined.log - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: logs path: logs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6185df836a19aa..f4d69b3d35d507 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,11 +17,11 @@ jobs: # runs-on: ubuntu-latest # timeout-minutes: 5 # steps: - # - uses: actions/checkout@v4 - # - uses: pnpm/action-setup@v3 + # - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 # with: - # version: 8 - # - uses: actions/setup-node@v4 + # version: 9 + # - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 # with: # node-version: lts/* # cache: 'pnpm' @@ -36,14 +36,11 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 permissions: - contents: read security-events: write steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 8 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: lts/* cache: 'pnpm' @@ -65,10 +62,8 @@ jobs: name: Validate PR title runs-on: ubuntu-latest timeout-minutes: 5 - permissions: - pull-requests: read steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ignoreLabels: | @@ -77,13 +72,12 @@ jobs: labeler: name: Pull Request Labeler - if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' }} + if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && github.repository == 'DIYgod/RSSHub' }} permissions: - contents: read pull-requests: write runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index d37f85eb592c72..8bc356a9311780 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -9,7 +9,6 @@ on: - 'lib/**' permissions: - contents: read id-token: write jobs: @@ -21,14 +20,11 @@ jobs: env: HUSKY: 0 steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - # pinned to 18 until https://github.com/compulim/version-from-git/issues/16 is fixed - node-version: 18 + node-version: lts/* cache: 'pnpm' registry-url: 'https://registry.npmjs.org' - name: Install dependencies (pnpm) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 3cc83b81f96f15..5033b76cbe3a14 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -12,9 +12,6 @@ on: # random HH:MM to avoid a load spike on GitHub Actions at 00:00 - cron: 21 20 * * * -permissions: - contents: read - jobs: semgrep: name: Scan @@ -23,15 +20,14 @@ jobs: image: returntocorp/semgrep if: (github.triggering_actor != 'dependabot[bot]') permissions: - contents: read security-events: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: semgrep ci --sarif > semgrep.sarif env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} - name: Upload SARIF file for GitHub Advanced Security Dashboard - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 with: sarif_file: semgrep.sarif if: always() diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index c11a215094233a..9f100f223ae81a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: # Don't stale issues days-before-issue-stale: -1 diff --git a/.github/workflows/test-full-routes.yml b/.github/workflows/test-full-routes.yml index 0a8b63164856dd..5d08e43e90c8b3 100644 --- a/.github/workflows/test-full-routes.yml +++ b/.github/workflows/test-full-routes.yml @@ -5,25 +5,20 @@ on: schedule: - cron: '0 0 * * *' -permissions: - contents: read - jobs: build: runs-on: ubuntu-latest name: Build assets - timeout-minutes: 60 + timeout-minutes: 120 permissions: contents: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install pnpm - uses: pnpm/action-setup@v3 - with: - version: 8 + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Use Node.js Active LTS - uses: actions/setup-node@v4 + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: lts/* cache: 'pnpm' @@ -35,7 +30,7 @@ jobs: continue-on-error: true run: pnpm vitest:fullroutes - name: Deploy - uses: peaceiris/actions-gh-pages@v4 + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./assets diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c67d7e01c29471..9841e57f2260f0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,33 +12,9 @@ on: pull_request: {} permissions: - contents: read + checks: write jobs: - fix-pnpmp-lock: - # workaround for https://github.com/dependabot/dependabot-core/issues/7258 - # until https://github.com/pnpm/pnpm/issues/6530 is fixed - if: github.triggering_actor == 'dependabot[bot]' && github.event_name == 'pull_request' - runs-on: ubuntu-latest - permissions: - pull-requests: write - contents: write - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - cache: 'pnpm' - - run: | - rm pnpm-lock.yaml - pnpm i - - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: 'chore: fix pnpm install' - vitest: runs-on: ubuntu-latest timeout-minutes: 10 @@ -51,14 +27,12 @@ jobs: strategy: fail-fast: false matrix: - node-version: [ 20, 22 ] + node-version: [ latest, lts/*, lts/-1 ] name: Vitest on Node ${{ matrix.node-version }} steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 8 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -69,12 +43,12 @@ jobs: - name: Build routes run: pnpm build - name: Test all and generate coverage - run: pnpm run vitest:coverage + run: pnpm run vitest:coverage --reporter=github-actions env: REDIS_URL: redis://localhost:${{ job.services.redis.ports[6379] }}/ - name: Upload coverage to Codecov - if: ${{ matrix.node-version == '20' }} - uses: codecov/codecov-action@v4 + if: ${{ matrix.node-version == 'lts/*' }} + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos as documented, but seems broken @@ -84,7 +58,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [ 20, 22 ] + node-version: [ latest, lts/*, lts/-1 ] chromium: - name: bundled Chromium dependency: '' @@ -97,11 +71,9 @@ jobs: environment: '{ "PUPPETEER_SKIP_DOWNLOAD": "1" }' name: Vitest puppeteer on Node ${{ matrix.node-version }} with ${{ matrix.chromium.name }} steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 8 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -138,23 +110,28 @@ jobs: all: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + attestations: write strategy: fail-fast: false matrix: - node-version: [ 20, 22 ] + node-version: [ 23, 22, 20 ] name: Build radar and maintainer on Node ${{ matrix.node-version }} steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v3 - with: - version: 8 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' - run: pnpm i - name: Build radar and maintainer run: npm run build + - name: Upload assets + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: generated-assets-${{ matrix.node-version }} + path: assets/build/ automerge: if: github.triggering_actor == 'dependabot[bot]' && github.event_name == 'pull_request' @@ -164,7 +141,7 @@ jobs: pull-requests: write contents: write steps: - - uses: fastify/github-action-merge-dependabot@v3 + - uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} target: patch diff --git a/.gitignore b/.gitignore index a5c841183ab023..368a4c547ba8e7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,5 +30,3 @@ package-lock.json # pnpm-lock.yaml yarn.lock yarn-error.log - -scripts/twitter-token/accounts.* diff --git a/.gitpod.yml b/.gitpod.yml index 0afafdad37c9c3..0ee2c93f22e2c0 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -32,7 +32,7 @@ vscode: - EditorConfig.EditorConfig - esbenp.prettier-vscode - deepscan.vscode-deepscan - - rangav.vscode-thunder-client - sonarsource.sonarlint-vscode + # - VASubasRaj.flashpost not available on Open VSX, Thunder Client is paywalled in WSL/Codespaces/SSH > 2.30.0 - unifiedjs.vscode-mdx # - ZihanLi.at-helper not available on Open VSX diff --git a/.husky/pre-commit b/.husky/pre-commit index 2312dc587f6118..c27d8893a99490 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -npx lint-staged +lint-staged diff --git a/.npmrc b/.npmrc index cafe685a112d33..74538ab74e5dd9 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ package-lock=true +package-manager-strict=false diff --git a/Dockerfile b/Dockerfile index ac4b0a6d9d0b17..bd96cee1503489 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:21-bookworm AS dep-builder +FROM node:22-bookworm 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. @@ -8,6 +8,8 @@ WORKDIR /app ARG USE_CHINA_NPM_REGISTRY=0 RUN \ set -ex && \ + npm install -g corepack@latest && \ + corepack enable pnpm && \ if [ "$USE_CHINA_NPM_REGISTRY" = 1 ]; then \ echo 'use npm mirror' && \ npm config set registry https://registry.npmmirror.com && \ @@ -23,7 +25,6 @@ COPY ./package.json /app/ RUN \ set -ex && \ export PUPPETEER_SKIP_DOWNLOAD=true && \ - npm install -g pnpm@8.15.7 && \ pnpm install --frozen-lockfile && \ pnpm rb @@ -33,7 +34,7 @@ FROM debian:bookworm-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:21-bookworm-slim is based on debian:bookworm-slim so this stage would not cause any additional download. +# node:22-bookworm-slim is based on debian:bookworm-slim so this stage would not cause any additional download. WORKDIR /ver COPY ./package.json /app/ @@ -45,7 +46,7 @@ RUN \ # --------------------------------------------------------------------------------------------------------------------- -FROM node:21-bookworm-slim AS docker-minifier +FROM node:22-bookworm-slim AS docker-minifier # The stage is used to further reduce the image size by removing unused files. WORKDIR /app @@ -79,7 +80,7 @@ RUN \ # --------------------------------------------------------------------------------------------------------------------- -FROM node:21-bookworm-slim AS chromium-downloader +FROM node:22-bookworm-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. @@ -102,7 +103,8 @@ RUN \ fi; \ echo 'Downloading Chromium...' && \ unset PUPPETEER_SKIP_DOWNLOAD && \ - npm install -g pnpm@8.15.7 && \ + npm install -g corepack@latest && \ + corepack use pnpm@latest-9 && \ pnpm add puppeteer@$(cat /app/.puppeteer_version) --save-prod && \ pnpm rb ; \ else \ @@ -111,12 +113,12 @@ RUN \ # --------------------------------------------------------------------------------------------------------------------- -FROM node:21-bookworm-slim AS app +FROM node:22-bookworm-slim AS app LABEL org.opencontainers.image.authors="https://github.com/DIYgod/RSSHub" -ENV NODE_ENV production -ENV TZ Asia/Shanghai +ENV NODE_ENV=production +ENV TZ=Asia/Shanghai WORKDIR /app @@ -132,7 +134,7 @@ RUN \ set -ex && \ apt-get update && \ apt-get install -yq --no-install-recommends \ - dumb-init git \ + dumb-init git curl \ ; \ if [ "$PUPPETEER_SKIP_DOWNLOAD" = 0 ]; then \ if [ "$TARGETPLATFORM" = 'linux/amd64' ]; then \ diff --git a/README.md b/README.md index 8eb2654555ec1d..defee8667f8dac 100644 --- a/README.md +++ b/README.md @@ -10,38 +10,17 @@ [](https://www.npmjs.com/package/rsshub) [](https://github.com/DIYgod/RSSHub/actions/workflows/test.yml?query=event%3Apush+branch%3Amaster) [](https://app.codecov.io/gh/DIYgod/RSSHub/branch/master) +[](https://github.com/DIYgod/RSSHub) -[](https://t.me/rsshub) [](https://t.me/awesomeRSSHub) [](https://twitter.com/intent/follow?screen_name=_RSSHub) +[](https://t.me/rsshub) [](https://t.me/awesomeRSSHub) [](https://x.com/intent/follow?screen_name=_RSSHub) ## Introduction -RSSHub is an open source, easy to use, and extensible RSS feed generator. It's capable of generating RSS feeds from pretty much everything. +RSSHub is the world's largest RSS network, consisting of over 5,000 global instances. RSSHub delivers millions of contents aggregated from all kinds of sources, our vibrant open source community is ensuring the deliver of RSSHub's new routes, new features and bug fixes. -RSSHub can be used with browser extension [RSSHub Radar](https://github.com/DIYgod/RSSHub-Radar) and mobile auxiliary app [RSSBud](https://github.com/Cay-Zhang/RSSBud) (iOS) and [RSSAid](https://github.com/LeetaoGoooo/RSSAid) (Android) - -[English docs](https://docs.rsshub.app) | [Telegram Group](https://t.me/rsshub) | [Telegram Channel](https://t.me/awesomeRSSHub) | [Twitter](https://twitter.com/intent/follow?screen_name=_RSSHub) | [中文文档](https://docs.rsshub.app/zh/) - -## Special Thanks - -### Special Sponsors - -
- -[](https://docs.rsshub.app/sponsor/) - -### Contributors - -[](https://github.com/DIYgod/RSSHub/graphs/contributors) - -Logo designer [sheldonrrr](https://dribbble.com/sheldonrrr) - -### Backers - -
-
-
-
-
{{$value}}
{{/each}} +{{ if info }}时长: {{itunes_duration}}
{{ intro }}+{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/4gamers/namespace.ts b/lib/routes/4gamers/namespace.ts index f133378e302117..ceb8f3e11c1ca3 100644 --- a/lib/routes/4gamers/namespace.ts +++ b/lib/routes/4gamers/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '4Gamers', url: 'www.4gamers.com.tw', + lang: 'zh-TW', }; diff --git a/lib/routes/4khd/article.ts b/lib/routes/4khd/article.ts new file mode 100644 index 00000000000000..c600a581ae7b78 --- /dev/null +++ b/lib/routes/4khd/article.ts @@ -0,0 +1,28 @@ +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { WPPost } from './types'; + +const processImages = ($) => { + $('a').each((_, elem) => { + const $elem = $(elem); + const largePhotoUrl = $elem.attr('href').replace('i0.wp.com/pic', 'img'); + if (largePhotoUrl) { + $elem.attr('href', largePhotoUrl); + $elem.find('img').attr('src', largePhotoUrl); + } + }); +}; + +function loadArticle(item: WPPost) { + const article = load(item.content.rendered); + processImages(article); + + return { + title: item.title.rendered, + description: article.html() ?? '', + pubDate: parseDate(item.date_gmt), + link: item.link, + }; +} + +export default loadArticle; diff --git a/lib/routes/4khd/category.ts b/lib/routes/4khd/category.ts new file mode 100644 index 00000000000000..539baad8be35a9 --- /dev/null +++ b/lib/routes/4khd/category.ts @@ -0,0 +1,48 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { SUB_NAME_PREFIX, SUB_URL } from './const'; +import loadArticle from './article'; +import { WPPost } from './types'; + +export const route: Route = { + path: '/category/:category', + categories: ['picture'], + example: '/4khd/category/cosplay', + parameters: { category: 'Category' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.4khd.com/pages/:category'], + target: '/category/:category', + }, + ], + name: 'Category', + maintainers: ['AiraNadih'], + handler, + url: 'www.4khd.com/', +}; + +async function handler(ctx) { + const limit = Number.parseInt(ctx.req.query('limit')) || 20; + const category = ctx.req.param('category'); + 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: posts } = await got(`${SUB_URL}wp-json/wp/v2/posts?categories=${categoryId}&per_page=${limit}`); + + return { + title: `${SUB_NAME_PREFIX} - Category: ${category}`, + link: categoryUrl, + item: posts.map((post) => loadArticle(post as WPPost)), + }; +} diff --git a/lib/routes/4khd/const.ts b/lib/routes/4khd/const.ts new file mode 100644 index 00000000000000..130430f17be0dc --- /dev/null +++ b/lib/routes/4khd/const.ts @@ -0,0 +1,4 @@ +const SUB_NAME_PREFIX = '4KHD'; +const SUB_URL = 'https://www.4khd.com/'; + +export { SUB_NAME_PREFIX, SUB_URL }; diff --git a/lib/routes/4khd/latest.ts b/lib/routes/4khd/latest.ts new file mode 100644 index 00000000000000..e718baf8bf0b66 --- /dev/null +++ b/lib/routes/4khd/latest.ts @@ -0,0 +1,41 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { SUB_NAME_PREFIX, SUB_URL } from './const'; +import loadArticle from './article'; +import { WPPost } from './types'; + +export const route: Route = { + path: '/', + categories: ['picture'], + example: '/4khd', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.4khd.com/'], + target: '', + }, + ], + name: 'Latest', + maintainers: ['AiraNadih'], + handler, + url: 'www.4khd.com/', +}; + +async function handler(ctx) { + const limit = Number.parseInt(ctx.req.query('limit')) || 20; + const { data: posts } = await got(`${SUB_URL}wp-json/wp/v2/posts?per_page=${limit}`); + + return { + title: `${SUB_NAME_PREFIX} - Latest`, + link: SUB_URL, + item: posts.map((post) => loadArticle(post as WPPost)), + }; +} diff --git a/lib/routes/4khd/namespace.ts b/lib/routes/4khd/namespace.ts new file mode 100644 index 00000000000000..c5bb29a8c2705a --- /dev/null +++ b/lib/routes/4khd/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '4KHD', + url: 'www.4khd.net', + description: '4KHD - HD Beautiful Girls', + lang: 'en', +}; diff --git a/lib/routes/4khd/types.ts b/lib/routes/4khd/types.ts new file mode 100644 index 00000000000000..d3ea3ac2a8cc26 --- /dev/null +++ b/lib/routes/4khd/types.ts @@ -0,0 +1,12 @@ +interface WPPost { + title: { + rendered: string; + }; + content: { + rendered: string; + }; + date_gmt: string; + link: string; +} + +export type { WPPost }; diff --git a/lib/routes/4ksj/forum.ts b/lib/routes/4ksj/forum.ts index 79c042020f91cf..a5584cebe32e82 100644 --- a/lib/routes/4ksj/forum.ts +++ b/lib/routes/4ksj/forum.ts @@ -3,13 +3,13 @@ import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import timezone from '@/utils/timezone'; import { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; import path from 'node:path'; -import iconv from 'iconv-lite'; +import md5 from '@/utils/md5'; export const route: Route = { path: '/:id?', @@ -19,14 +19,24 @@ export const route: Route = { handler, example: '/4ksj/4k-uhd-1', parameters: { id: '分类 id,默认为最新4K电影' }, - description: `:::tip + description: `::: tip 若订阅 [最新 4K 电影](https://www.4ksj.com/4k-uhd-1.html),网址为 \`https://www.4ksj.com/4k-uhd-1.html\`。截取 \`https://www.4ksj.com/\` 到末尾 \`.html\` 的部分 \`4k-uhd-1\` 作为参数,此时路由为 [\`/4ksj/4k-uhd-1\`](https://rsshub.app/4ksj/4k-uhd-1)。 若订阅子分类 [Dolby Vision 动作 4K 电影](https://www.4ksj.com/4k-uhd-s7-display-3-dytypes-1-1.html),网址为 \`https://www.4ksj.com/4k-uhd-s7-display-3-dytypes-1-1.html\`。截取 \`https://www.4ksj.com/forum-\` 到末尾 \`.html\` 的部分 \`4kdianying-s7-dianyingbiaozhun-3-dytypes-9-1\` 作为参数,此时路由为 [\`/4ksj/4k-uhd-s7-display-3-dytypes-1-1\`](https://rsshub.app/4ksj/4k-uhd-s7-display-3-dytypes-1-1)。 - :::`, +:::`, categories: ['multimedia'], }; +function stringtoHex(acSTR) { + let val = ''; + for (let i = 0; i <= acSTR.length - 1; i++) { + const str = acSTR.charAt(i); + const code = str.codePointAt(); + val += code; + } + return val; +} + async function handler(ctx) { const { id = '4k-uhd-1' } = ctx.req.param(); const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 25; @@ -34,11 +44,13 @@ async function handler(ctx) { const rootUrl = 'https://www.4ksj.com'; const currentUrl = new URL(`${id}.html`, rootUrl).href; - const { data: response } = await got(currentUrl, { - responseType: 'buffer', + const response = await ofetch(currentUrl, { + responseType: 'arrayBuffer', }); - const $ = load(iconv.decode(response, 'gbk')); + const decoder = new TextDecoder('gbk'); + + const $ = load(decoder.decode(response)); const language = 'zh'; const image = $('div.nexlogo img').prop('src'); @@ -54,14 +66,42 @@ async function handler(ctx) { }; }); + const getCookie = () => + cache.tryGet('4ksj:cookie', async () => { + const response = await ofetch(items[0].link); + const $ = load(response); + const scriptPath = $('script').attr('src')!; + const scriptUrl = new URL(scriptPath, rootUrl).href; + + const scriptResponse = await ofetch(scriptUrl); + const key = scriptResponse.match(/{var key="(.*?)"/)?.[1]; + const value = scriptResponse.match(/",value="(.*?)"/)?.[1]; + const getPath = scriptResponse.match(/\.get\("(.*?&key=)"/)?.[1]; + + if (!key || !value || !getPath) { + throw new Error('Failed to get cookie'); + } + + const cookieResponse = await ofetch.raw(`${rootUrl}${getPath}${key}&value=${md5(stringtoHex(value))}`); + return cookieResponse.headers + .getSetCookie() + .map((c) => c.split(';')[0]) + .join('; '); + }); + + const cookie = await getCookie(); + items = await Promise.all( items.map((item) => cache.tryGet(item.link, async () => { - const { data: detailResponse } = await got(item.link, { - responseType: 'buffer', + const detailResponse = await ofetch(item.link, { + responseType: 'arrayBuffer', + headers: { + Cookie: cookie as string, + }, }); - const $$ = load(iconv.decode(detailResponse, 'gbk')); + const $$ = load(decoder.decode(detailResponse)); $$('div.nex_drama_intros em').first().remove(); $$('strong font').each((_, el) => { @@ -86,14 +126,8 @@ async function handler(ctx) { const value = li.find('span').length === 0 ? li.contents().last().text().trim() : li.find('span').text().trim(); return { [key]: value }; - }) - .reduce( - (obj, item) => ({ - ...obj, - ...item, - }), - {} - ); + }); + const mergedDetails = Object.assign({}, ...details); const links = $$('td.t_f ignore_js_op').length === 0 @@ -154,17 +188,17 @@ async function handler(ctx) { ] : undefined, title, - keys: Object.keys(details), - details, + keys: Object.keys(mergedDetails), + details: mergedDetails, description, info: $$('div.nex_drama_sums').html(), links, }); item.pubDate = timezone(parseDate(pubDate, 'YYYY-M-D HH:mm:ss'), +8); - item.category = Object.values(details) + item.category = Object.values(mergedDetails) .flatMap((c) => c.split(/\s/)) .filter(Boolean); - item.author = details['导演']; + item.author = mergedDetails['导演']; item.content = { html: description, text: $$('div.nex_drama_intros').text(), diff --git a/lib/routes/4ksj/namespace.ts b/lib/routes/4ksj/namespace.ts index 1965e275fc34c4..4b2b90dc42066a 100644 --- a/lib/routes/4ksj/namespace.ts +++ b/lib/routes/4ksj/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '4k 世界', url: '4ksj.com', + lang: 'zh-CN', }; diff --git a/lib/routes/4kup/article.ts b/lib/routes/4kup/article.ts new file mode 100644 index 00000000000000..fbdbbbb52e9eac --- /dev/null +++ b/lib/routes/4kup/article.ts @@ -0,0 +1,29 @@ +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import { WPPost } from './types'; + +const processLazyImages = ($) => { + $('a.thumb-photo').each((_, elem) => { + const $elem = $(elem); + const largePhotoUrl = $elem.attr('href'); + if (largePhotoUrl) { + $elem.find('img').attr('src', largePhotoUrl); + } + }); + + $('.caption').remove(); +}; + +function loadArticle(item: WPPost) { + const article = load(item.content.rendered); + processLazyImages(article); + + return { + title: item.title.rendered, + description: article.html() ?? '', + pubDate: parseDate(item.date_gmt), + link: item.link, + }; +} + +export default loadArticle; diff --git a/lib/routes/4kup/category.ts b/lib/routes/4kup/category.ts new file mode 100644 index 00000000000000..9b227ec329397b --- /dev/null +++ b/lib/routes/4kup/category.ts @@ -0,0 +1,47 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { SUB_NAME_PREFIX, SUB_URL } from './const'; +import loadArticle from './article'; +import { WPPost } from './types'; + +export const route: Route = { + path: '/category/:category', + categories: ['picture'], + example: '/4kup/category/coser', + parameters: { category: 'Category' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['4kup.net/category/:category'], + target: '/category/:category', + }, + ], + name: 'Category', + maintainers: ['AiraNadih'], + handler, + url: '4kup.net/', +}; + +async function handler(ctx) { + const limit = Number.parseInt(ctx.req.query('limit')) || 20; + 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: posts } = await got(`${SUB_URL}wp-json/wp/v2/posts?categories=${categoryId}&per_page=${limit}`); + + return { + title: `${SUB_NAME_PREFIX} - Category: ${category}`, + link: categoryUrl, + item: posts.map((post) => loadArticle(post as WPPost)), + }; +} diff --git a/lib/routes/4kup/const.ts b/lib/routes/4kup/const.ts new file mode 100644 index 00000000000000..52c67eacb74a25 --- /dev/null +++ b/lib/routes/4kup/const.ts @@ -0,0 +1,4 @@ +const SUB_NAME_PREFIX = '4KUP'; +const SUB_URL = 'https://4kup.net/'; + +export { SUB_NAME_PREFIX, SUB_URL }; diff --git a/lib/routes/4kup/latest.ts b/lib/routes/4kup/latest.ts new file mode 100644 index 00000000000000..12da303c6173c1 --- /dev/null +++ b/lib/routes/4kup/latest.ts @@ -0,0 +1,41 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { SUB_NAME_PREFIX, SUB_URL } from './const'; +import loadArticle from './article'; +import { WPPost } from './types'; + +export const route: Route = { + path: '/', + categories: ['picture'], + example: '/4kup', + parameters: {}, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['4kup.net/'], + target: '', + }, + ], + name: 'Latest', + maintainers: ['AiraNadih'], + handler, + url: '4kup.net/', +}; + +async function handler(ctx) { + const limit = Number.parseInt(ctx.req.query('limit')) || 20; + const { data: posts } = await got(`${SUB_URL}wp-json/wp/v2/posts?per_page=${limit}`); + + return { + title: `${SUB_NAME_PREFIX} - Latest`, + link: SUB_URL, + item: posts.map((post) => loadArticle(post as WPPost)), + }; +} diff --git a/lib/routes/4kup/namespace.ts b/lib/routes/4kup/namespace.ts new file mode 100644 index 00000000000000..41b591a0d5eb53 --- /dev/null +++ b/lib/routes/4kup/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '4KUP', + url: '4kup.net', + description: '4KUP - Beautiful Girls Collection', + lang: 'en', +}; diff --git a/lib/routes/4kup/popular.ts b/lib/routes/4kup/popular.ts new file mode 100644 index 00000000000000..326673d1b007ce --- /dev/null +++ b/lib/routes/4kup/popular.ts @@ -0,0 +1,81 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { SUB_NAME_PREFIX, SUB_URL } from './const'; +import loadArticle from './article'; +import { WPPost } from './types'; + +export const route: Route = { + path: '/popular/:period', + categories: ['picture'], + example: '/4kup/popular/7', + parameters: { period: 'Days' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['4kup.net/:period'], + target: '/popular/:period', + }, + ], + name: 'Popular', + maintainers: ['AiraNadih'], + handler, + url: '4kup.net/', +}; + +function getPeriodConfig(period) { + if (period === '7') { + return { + url: `${SUB_URL}hot-of-week/`, + range: 'last7days', + title: `${SUB_NAME_PREFIX} - Top views in 7 days`, + }; + } else if (period === '30') { + return { + url: `${SUB_URL}hot-of-month/`, + range: 'last30days', + title: `${SUB_NAME_PREFIX} - Top views in 30 days`, + }; + } + return { + url: `${SUB_URL}most-view/`, + range: `all`, + title: `${SUB_NAME_PREFIX} - Most views`, + }; +} + +async function handler(ctx) { + const limit = Number.parseInt(ctx.req.query('limit')) || 20; + const period = ctx.req.param('period'); + + const { url, range, title } = getPeriodConfig(period); + + const { data } = await got.post(`${SUB_URL}wp-json/wordpress-popular-posts/v2/widget`, { + json: { + limit, + range, + order_by: 'views', + }, + }); + + const $ = load(data.widget); + const links = $('.wpp-list li') + .toArray() + .map((post) => $(post).find('.wpp-post-title').attr('href')) + .filter((link) => link !== undefined); + const slugs = links.map((link) => link.split('/').findLast(Boolean)); + const { data: posts } = await got(`${SUB_URL}wp-json/wp/v2/posts?slug=${slugs.join(',')}&per_page=${limit}`); + + return { + title, + link: url, + item: posts.map((post) => loadArticle(post as WPPost)), + }; +} diff --git a/lib/routes/4kup/tag.ts b/lib/routes/4kup/tag.ts new file mode 100644 index 00000000000000..93acd7ec98e282 --- /dev/null +++ b/lib/routes/4kup/tag.ts @@ -0,0 +1,47 @@ +import { Route } from '@/types'; +import got from '@/utils/got'; +import { SUB_NAME_PREFIX, SUB_URL } from './const'; +import loadArticle from './article'; +import { WPPost } from './types'; + +export const route: Route = { + path: '/tag/:tag', + categories: ['picture'], + example: '/4kup/tag/asian', + parameters: { tag: 'Tag' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['4kup.net/tag/:tag'], + target: '/tag/:tag', + }, + ], + name: 'Tag', + maintainers: ['AiraNadih'], + handler, + url: '4kup.net/', +}; + +async function handler(ctx) { + const limit = Number.parseInt(ctx.req.query('limit')) || 20; + 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: posts } = await got(`${SUB_URL}wp-json/wp/v2/posts?tags=${tagId}&per_page=${limit}`); + + return { + title: `${SUB_NAME_PREFIX} - Tag: ${tag}`, + link: tagUrl, + item: posts.map((post) => loadArticle(post as WPPost)), + }; +} diff --git a/lib/routes/4kup/types.ts b/lib/routes/4kup/types.ts new file mode 100644 index 00000000000000..d3ea3ac2a8cc26 --- /dev/null +++ b/lib/routes/4kup/types.ts @@ -0,0 +1,12 @@ +interface WPPost { + title: { + rendered: string; + }; + content: { + rendered: string; + }; + date_gmt: string; + link: string; +} + +export type { WPPost }; diff --git a/lib/routes/500px/namespace.ts b/lib/routes/500px/namespace.ts index e4783d9aa476b7..0ef51cf33e4b08 100644 --- a/lib/routes/500px/namespace.ts +++ b/lib/routes/500px/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '500px 摄影社区', url: '500px.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/500px/tribe-set.ts b/lib/routes/500px/tribe-set.ts index c157f336940a7a..72cb646766cb6d 100644 --- a/lib/routes/500px/tribe-set.ts +++ b/lib/routes/500px/tribe-set.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); @@ -10,7 +10,8 @@ import { baseUrl, getTribeDetail, getTribeSets } from './utils'; export const route: Route = { path: '/tribe/set/:id', - categories: ['picture'], + categories: ['picture', 'popular'], + view: ViewType.Pictures, example: '/500px/tribe/set/f5de0b8aa6d54ec486f5e79616418001', parameters: { id: '部落 ID' }, name: '部落影集', diff --git a/lib/routes/50forum/namespace.ts b/lib/routes/50forum/namespace.ts index 7774e24e56c7fb..267b3941753636 100644 --- a/lib/routes/50forum/namespace.ts +++ b/lib/routes/50forum/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '经济 50 人论坛', url: '50forum.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/51cto/namespace.ts b/lib/routes/51cto/namespace.ts index 96d3ff4ed31a85..279fbdf40ec40f 100644 --- a/lib/routes/51cto/namespace.ts +++ b/lib/routes/51cto/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '51CTO', url: '51cto.com', + lang: 'zh-CN', }; diff --git a/lib/routes/51cto/recommend.ts b/lib/routes/51cto/recommend.ts index fa5d426fb75182..368280b31e7ed5 100644 --- a/lib/routes/51cto/recommend.ts +++ b/lib/routes/51cto/recommend.ts @@ -2,6 +2,10 @@ import { Route } from '@/types'; import { parseDate } from '@/utils/parse-date'; import got from '@/utils/got'; import { getToken, sign } from './utils'; +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import logger from '@/utils/logger'; export const route: Route = { path: '/index/recommend', @@ -13,11 +17,46 @@ export const route: Route = { }, ], name: '推荐', - maintainers: ['cnkmmk'], + maintainers: ['cnkmmk', 'ovo-tim'], handler, url: '51cto.com/', }; +const pattern = /'(WTKkN|bOYDu|wyeCN)':\s*(\d+)/g; + +async function getFullcontent(item, cookie = '') { + let fullContent: null | string = null; + const articleResponse = await ofetch(item.url, { + headers: { + cookie, + }, + }); + const $ = load(articleResponse); + + fullContent = new URL(item.url).host === 'ost.51cto.com' ? $('.posts-content').html() : $('article').html(); + + if (!fullContent && cookie === '') { + // If fullContent is null and haven't tried to request with cookie, try to get fullContent with 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 [v1, v2, v3] = matches; + const cookie = '__tst_status=' + (v1 + v2 + v3) + '#;'; + return await getFullcontent(item, cookie); + } catch (error) { + logger.error(error); + } + } + + return { + title: item.title, + link: item.url, + pubDate: parseDate(item.pubdate, +8), + description: fullContent || item.abstract, // Return item.abstract if fullContent is null + }; +} + async function handler(ctx) { const url = 'https://api-media.51cto.com'; const requestPath = 'index/index/recommend'; @@ -29,6 +68,7 @@ async function handler(ctx) { limit_time: 0, name_en: '', }; + const response = await got(`${url}/${requestPath}`, { searchParams: { ...params, @@ -38,15 +78,13 @@ async function handler(ctx) { }, }); const list = response.data.data.data.list; + + const items = await Promise.all(list.map((item) => cache.tryGet(item.url, async () => await getFullcontent(item)))); + return { title: '51CTO', link: 'https://www.51cto.com/', description: '51cto - 推荐', - item: list.map((item) => ({ - title: item.title, - link: item.url, - pubDate: parseDate(item.pubdate, +8), - description: item.abstract, - })), + item: items, }; } diff --git a/lib/routes/51read/article.ts b/lib/routes/51read/article.ts new file mode 100644 index 00000000000000..6240362fa8b622 --- /dev/null +++ b/lib/routes/51read/article.ts @@ -0,0 +1,82 @@ +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import type { Route, DataItem } from '@/types'; + +export const route: Route = { + path: '/article/:id', + name: '章节', + url: 'm.51read.org', + maintainers: ['lazwa34'], + example: '/51read/article/152685', + parameters: { id: '小说 id, 可在对应小说页 URL 中找到' }, + categories: ['reading'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['m.51read.org/xiaoshuo/:id'], + target: '/article/:id', + }, + { + source: ['51read.org/xiaoshuo/:id'], + target: '/article/:id', + }, + ], + handler, +}; + +async function handler(ctx) { + const { id } = ctx.req.param(); + const link = `https://m.51read.org/xiaoshuo/${id}`; + const $book = load(await ofetch(link)); + + const chapter = `https://m.51read.org/zhangjiemulu/${id}`; + const $chapter = load(await ofetch(chapter)); + + const pageLength = $chapter('.ml-page select') + .find('option') + .toArray() + .map((option) => option.attribs.value).length; + + const item = await createItem(chapter, pageLength); + + return { + title: $book('h1').text(), + description: $book('.bi-cot p').text(), + link, + item, + image: $book('.bi-img img').attr('src'), + author: $book('.bi-wt a').text(), + language: 'zh-cn', + }; +} + +const createItem = async (baseUrl: string, page: number) => { + const url = `${baseUrl}/${page}`; + const $latest = load(await ofetch(url)); + const item = await Promise.all( + $latest('.kb-jp li>a') + .toArray() + .map((chapter) => buildItem(chapter.attribs.href)) + .toReversed() + ); + return item; +}; + +const buildItem = (url: string) => + cache.tryGet(url, async () => { + const $ = load(await ofetch(url)); + + return { + title: $('h1').text(), + description: $('.kb-cot').html() || '', + link: url, + }; + }) as Promise
艺人: ${artist}
+专辑: ${album}
+发行公司: ${company}
+格式: ${format}
+发行日期: ${releaseDate}
+ `, + link: albumLink ? `https://www.5music.com.tw/${albumLink}` : url, + pubDate: parseDate(releaseDate), + category: format, + author: artist, + }; + }); + + return { + title: '五大唱片 - 新货上架', + link: url, + item: items, + language: 'zh-tw', + }; +} diff --git a/lib/routes/5music/namespace.ts b/lib/routes/5music/namespace.ts new file mode 100644 index 00000000000000..a553165dcc82df --- /dev/null +++ b/lib/routes/5music/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '五大唱片', + url: '5music.com.tw', + lang: 'zh-TW', + categories: ['shopping'], + description: '五大唱片是台湾五大唱片股份有限公司的简称,成立于1990年,是台湾最大的唱片公司之一。', +}; diff --git a/lib/routes/69shu/article.ts b/lib/routes/69shu/article.ts new file mode 100644 index 00000000000000..4371e35dce616f --- /dev/null +++ b/lib/routes/69shu/article.ts @@ -0,0 +1,96 @@ +import { load } from 'cheerio'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import type { Route, DataItem } from '@/types'; + +export const route: Route = { + path: '/article/:id', + name: '章节', + url: 'www.69shuba.cx', + maintainers: ['eternasuno'], + example: '/69shu/article/47117', + parameters: { id: '小说 id, 可在对应小说页 URL 中找到' }, + categories: ['reading'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.69shuba.cx/book/:id.htm'], + target: '/article/:id', + }, + ], + handler: async (ctx) => { + const { id } = ctx.req.param(); + const link = `https://www.69shuba.cx/book/${id}.htm`; + const $ = load(await get(link)); + + const item = await Promise.all( + $('.qustime li>a') + .toArray() + .map((chapter) => createItem(chapter.attribs.href)) + ); + + return { + title: $('h1>a').text(), + description: $('.navtxt>p:first-of-type').text(), + link, + item, + image: $('.bookimg2>img').attr('src'), + author: $('.booknav2>p:first-of-type>a').text(), + language: 'zh-cn', + }; + }, +}; + +const createItem = (url: string) => + cache.tryGet(url, async () => { + const $ = load(await get(url)); + 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, + description: decrypt($('.txtnav').html() || '', articleid, chapterid, decryptionMap), + link: url, + }; + }) as Promise' + author.name + author.authorBio.replaceAll(/^
/g, ' ')).join(''); + const authorsBio = data.authors.map((author) => '
' + author.name + author.authorBio.replaceAll(/^
/g, ' ')).join(''); item.description = art(path.join(__dirname, 'templates/essay.art'), { banner, authorsBio, content: capture.html() }); } diff --git a/lib/routes/afdian/dynamic.ts b/lib/routes/afdian/dynamic.ts index 2819a48a9e5c65..539279154e22d1 100644 --- a/lib/routes/afdian/dynamic.ts +++ b/lib/routes/afdian/dynamic.ts @@ -13,7 +13,7 @@ export const route: Route = { async function handler(ctx) { const url_slug = ctx.req.param('uid').replace('@', ''); - const baseUrl = 'https://afdian.net'; + const baseUrl = 'https://afdian.com'; const userInfoRes = await got(`${baseUrl}/api/user/get-profile-by-slug`, { searchParams: { url_slug, diff --git a/lib/routes/afdian/explore.ts b/lib/routes/afdian/explore.ts index 14437242a98ea8..e212ca4066c320 100644 --- a/lib/routes/afdian/explore.ts +++ b/lib/routes/afdian/explore.ts @@ -35,21 +35,21 @@ export const route: Route = { maintainers: ['sanmmm'], description: `分类 - | 推荐 | 最热 | - | ---- | ---- | - | rec | hot | - - 目录类型 - - | 所有 | 绘画 | 视频 | 写作 | 游戏 | 音乐 | 播客 | 摄影 | 技术 | Vtuber | 舞蹈 | 体育 | 旅游 | 美食 | 时尚 | 数码 | 动画 | 其他 | - | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | - | 所有 | 绘画 | 视频 | 写作 | 游戏 | 音乐 | 播客 | 摄影 | 技术 | Vtuber | 舞蹈 | 体育 | 旅游 | 美食 | 时尚 | 数码 | 动画 | 其他 |`, +| 推荐 | 最热 | +| ---- | ---- | +| rec | hot | + + 目录类型 + +| 所有 | 绘画 | 视频 | 写作 | 游戏 | 音乐 | 播客 | 摄影 | 技术 | Vtuber | 舞蹈 | 体育 | 旅游 | 美食 | 时尚 | 数码 | 动画 | 其他 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| 所有 | 绘画 | 视频 | 写作 | 游戏 | 音乐 | 播客 | 摄影 | 技术 | Vtuber | 舞蹈 | 体育 | 旅游 | 美食 | 时尚 | 数码 | 动画 | 其他 |`, handler, }; async function handler(ctx) { const { type = 'rec', category = '所有' } = ctx.req.param(); - const baseUrl = 'https://afdian.net'; + const baseUrl = 'https://afdian.com'; const link = `${baseUrl}/api/creator/list`; const res = await got(link, { searchParams: { diff --git a/lib/routes/afdian/namespace.ts b/lib/routes/afdian/namespace.ts index 3c4d181d72f71c..43af5fb4c42cd4 100644 --- a/lib/routes/afdian/namespace.ts +++ b/lib/routes/afdian/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '爱发电', url: 'afdian.net', + lang: 'zh-CN', }; diff --git a/lib/routes/afr/latest.ts b/lib/routes/afr/latest.ts new file mode 100644 index 00000000000000..b656755ceccfc8 --- /dev/null +++ b/lib/routes/afr/latest.ts @@ -0,0 +1,69 @@ +import { Route } from '@/types'; +import type { Context } from 'hono'; + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { assetsConnectionByCriteriaQuery } from './query'; +import { getItem } from './utils'; + +export const route: Route = { + path: '/latest', + categories: ['traditional-media'], + example: '/afr/latest', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.afr.com/latest', 'www.afr.com/'], + }, + ], + name: 'Latest', + maintainers: ['TonyRL'], + handler, + url: 'www.afr.com/latest', +}; + +async function handler(ctx: Context) { + const limit = Number.parseInt(ctx.req.query('limit') ?? '10'); + const response = await ofetch('https://api.afr.com/graphql', { + query: { + query: assetsConnectionByCriteriaQuery, + operationName: 'assetsConnectionByCriteria', + variables: { + brand: 'afr', + first: limit, + render: 'web', + types: ['article', 'bespoke', 'featureArticle', 'liveArticle', 'video'], + after: '', + }, + }, + }); + + const list = response.data.assetsConnectionByCriteria.edges.map(({ node }) => ({ + title: node.asset.headlines.headline, + description: node.asset.about, + link: `https://www.afr.com${node.urls.published.afr.path}`, + pubDate: parseDate(node.dates.firstPublished), + updated: parseDate(node.dates.modified), + author: node.asset.byline, + category: [node.tags.primary.displayName, ...node.tags.secondary.map((tag) => tag.displayName)], + image: node.featuredImages && `https://static.ffx.io/images/${node.featuredImages.landscape16x9.data.id}`, + })); + + const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => getItem(item)))); + + return { + title: 'Latest | The Australian Financial Review | AFR', + description: 'The latest news, events, analysis and opinion from The Australian Financial Review', + image: 'https://www.afr.com/apple-touch-icon-1024x1024.png', + link: 'https://www.afr.com/latest', + item: items, + }; +} diff --git a/lib/routes/afr/namespace.ts b/lib/routes/afr/namespace.ts new file mode 100644 index 00000000000000..d6fd9b647e2165 --- /dev/null +++ b/lib/routes/afr/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'The Australian Financial Review', + url: 'afr.com', + lang: 'en', +}; diff --git a/lib/routes/afr/navigation.ts b/lib/routes/afr/navigation.ts new file mode 100644 index 00000000000000..cbe7421a296b16 --- /dev/null +++ b/lib/routes/afr/navigation.ts @@ -0,0 +1,75 @@ +import { Route } from '@/types'; +import type { Context } from 'hono'; + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import { pageByNavigationPathQuery } from './query'; +import { getItem } from './utils'; + +export const route: Route = { + path: '/navigation/:path{.+}', + categories: ['traditional-media'], + example: '/afr/navigation/markets', + parameters: { + path: 'Navigation path, can be found in the URL of the page', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.afr.com/path*'], + }, + ], + name: 'Navigation', + maintainers: ['TonyRL'], + handler, + url: 'www.afr.com', +}; + +async function handler(ctx: Context) { + const { path } = ctx.req.param(); + const limit = Number.parseInt(ctx.req.query('limit') ?? '10'); + + const response = await ofetch('https://api.afr.com/api/content-audience/afr/graphql', { + query: { + query: pageByNavigationPathQuery, + operationName: 'pageByNavigationPath', + variables: { + input: { brandKey: 'afr', navigationPath: `/${path}`, renderName: 'web' }, + firstStories: limit, + afterStories: '', + }, + }, + }); + + const list = response.data.pageByNavigationPath.page.latestStoriesConnection.edges.map(({ node }) => ({ + title: node.headlines.headline, + description: node.overview.about, + link: `https://www.afr.com${node.urls.canonical.path}`, + pubDate: parseDate(node.dates.firstPublished), + updated: parseDate(node.dates.modified), + author: node.byline + .filter((byline) => byline.type === 'AUTHOR') + .map((byline) => byline.author.name) + .join(', '), + category: [node.tags.primary.displayName, ...node.tags.secondary.map((tag) => tag.displayName)], + image: node.images && `https://static.ffx.io/images/${node.images.landscape16x9.mediaId}`, + })); + + const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => getItem(item)))); + + return { + title: response.data.pageByNavigationPath.page.seo.title, + description: response.data.pageByNavigationPath.page.seo.description, + image: 'https://www.afr.com/apple-touch-icon-1024x1024.png', + link: `https://www.afr.com/${path}`, + item: items, + }; +} diff --git a/lib/routes/afr/query.ts b/lib/routes/afr/query.ts new file mode 100644 index 00000000000000..a596f8fc4f70dd --- /dev/null +++ b/lib/routes/afr/query.ts @@ -0,0 +1,349 @@ +export const pageByNavigationPathQuery = `query pageByNavigationPath( + $input: PageByNavigationPathInput! + $firstStories: Int + $afterStories: Cursor + ) { + pageByNavigationPath(input: $input) { + error { + message + type { + class + ... on ErrorTypeInvalidRequest { + fields { + field + message + } + } + } + } + page { + ads { + suppress + } + description + id + latestStoriesConnection(first: $firstStories, after: $afterStories) { + edges { + node { + byline { + ...AssetBylineFragment + } + headlines { + headline + } + ads { + sponsor { + name + } + } + overview { + about + label + } + type + dates { + firstPublished + published + } + id + publicId + images { + ...AssetImagesFragmentAudience + } + tags { + primary { + ...TagFragmentAudience + } + secondary { + ...TagFragmentAudience + } + } + urls { + ...AssetUrlsAudienceFragment + } + } + } + pageInfo { + endCursor + hasNextPage + } + } + name + seo { + canonical { + brand { + key + } + } + description + title + } + social { + image { + height + url + width + } + } + } + redirect + } + } + fragment AssetBylineFragment on AssetByline { + type + ... on AssetBylineAuthor { + author { + name + publicId + profile { + avatar + bio + body + canonical { + brand { + key + } + } + email + socials { + facebook { + publicId + } + twitter { + publicId + } + } + title + } + } + } + ... on AssetBylineName { + name + } + } + fragment AssetImagesFragmentAudience on ImageRenditions { + landscape16x9 { + ...ImageFragmentAudience + } + landscape3x2 { + ...ImageFragmentAudience + } + portrait2x3 { + ...ImageFragmentAudience + } + square1x1 { + ...ImageFragmentAudience + } + } + fragment ImageFragmentAudience on ImageRendition { + altText + animated + caption + credit + crop { + offsetX + offsetY + width + zoom + } + mediaId + mimeType + source + type + } + fragment AssetUrlsAudienceFragment on AssetURLs { + canonical { + brand { + key + } + path + } + external { + url + } + published { + brand { + key + } + path + } + } + fragment TagFragmentAudience on Tag { + company { + exchangeCode + stockCode + } + context { + name + } + description + displayName + externalEntities { + google { + placeId + } + wikipedia { + publicId + url + } + } + id + location { + latitude + longitude + postalCode + state + } + name + publicId + seo { + description + title + } + urls { + canonical { + brand { + key + } + path + } + published { + brand { + key + } + path + } + } + }`; + +export const assetsConnectionByCriteriaQuery = `query assetsConnectionByCriteria( + $after: ID + $brand: Brand! + $categories: [Int!] + $first: Int! + $render: Render! + $types: [AssetType!]! + ) { + assetsConnectionByCriteria( + after: $after + brand: $brand + categories: $categories + first: $first + render: $render + types: $types + ) { + edges { + cursor + node { + ...AssetFragment + sponsor { + name + } + } + } + error { + message + type { + class + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + fragment AssetFragment on Asset { + asset { + about + byline + duration + headlines { + headline + } + live + } + assetType + dates { + firstPublished + modified + published + } + id + featuredImages { + landscape16x9 { + ...ImageFragment + } + landscape3x2 { + ...ImageFragment + } + portrait2x3 { + ...ImageFragment + } + square1x1 { + ...ImageFragment + } + } + label + tags { + primary: primaryTag { + ...AssetTag + } + secondary { + ...AssetTag + } + } + urls { + ...AssetURLs + } + } + fragment AssetTag on AssetTagDetails { + ...AssetTagAudience + shortID + slug + } + fragment AssetTagAudience on AssetTagDetails { + company { + exchangeCode + stockCode + } + context + displayName + id + name + urls { + canonical { + brand + path + } + published { + afr { + path + } + } + } + } + fragment AssetURLs on AssetURLs { + canonical { + brand + path + } + published { + afr { + path + } + } + } + fragment ImageFragment on Image { + data { + altText + aspect + autocrop + caption + cropWidth + id + offsetX + offsetY + zoom + } + }`; diff --git a/lib/routes/afr/utils.ts b/lib/routes/afr/utils.ts new file mode 100644 index 00000000000000..c055ae9e70d29a --- /dev/null +++ b/lib/routes/afr/utils.ts @@ -0,0 +1,80 @@ +import * as cheerio from 'cheerio'; +import ofetch from '@/utils/ofetch'; + +export const getItem = async (item) => { + const response = await ofetch(item.link); + const $ = cheerio.load(response); + + const reduxState = JSON.parse($('script#__REDUX_STATE__').text().replaceAll(':undefined', ':null').match('__REDUX_STATE__=(.*);')?.[1] || '{}'); + + const content = reduxState.page.content; + const asset = content.asset; + + switch (content.assetType) { + case 'liveArticle': + item.description = asset.posts.map((post) => `
{{ intro }}+{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/ahjzu/namespace.ts b/lib/routes/ahjzu/namespace.ts index 2efc11070943a0..98bf403952ec20 100644 --- a/lib/routes/ahjzu/namespace.ts +++ b/lib/routes/ahjzu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '安徽建筑大学', url: 'news.ahjzu.edu.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/aibase/discover.ts b/lib/routes/aibase/discover.ts new file mode 100644 index 00000000000000..09dca068ac730b --- /dev/null +++ b/lib/routes/aibase/discover.ts @@ -0,0 +1,388 @@ +import { Route } from '@/types'; + +import ofetch from '@/utils/ofetch'; +import { load } from 'cheerio'; + +import { rootUrl, buildApiUrl, processItems } from './util'; + +export const handler = async (ctx) => { + const { id } = ctx.req.param(); + + const [pid, sid] = id?.split(/-/) ?? [undefined, undefined]; + + const limit = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const currentUrl = new URL(`discover${id ? `/${id}` : ''}`, rootUrl).href; + + const currentHtml = await ofetch(currentUrl); + + const $ = load(currentHtml); + + const { apiRecommListUrl, apiRecommProcUrl, apiTagProcUrl } = await buildApiUrl($); + + let ptag, stag; + let isTag = !!(pid && sid); + + if (isTag) { + const apiRecommList = await ofetch(apiRecommListUrl); + + const recommList = apiRecommList?.data?.results ?? []; + + const parentTag = recommList.find((t) => String(t.Id) === pid); + const subTag = parentTag ? parentTag.sublist.find((t) => String(t.Id) === sid) : undefined; + + ptag = parentTag?.tag ?? parentTag?.alias ?? undefined; + stag = subTag?.tag ?? subTag?.alias ?? undefined; + + isTag = !!(ptag && stag); + } + + const query = { + page: 1, + pagesize: limit, + ticket: '', + }; + + const { + data: { results: apiProcs }, + } = await (isTag + ? ofetch(apiRecommProcUrl, { + query: { + ...query, + ptag, + stag, + }, + }) + : ofetch(apiTagProcUrl, { + query: { + ...query, + f: 'id', + o: 'desc', + }, + })); + + const items = processItems(apiProcs?.slice(0, limit) ?? []); + + const image = new URL($('img.logo').prop('src'), rootUrl).href; + + const author = $('title').text().split(/_/).pop(); + + return { + title: `${author}${isTag ? ` | ${ptag} - ${stag}` : ''}`, + description: $('meta[property="og:description"]').prop('content'), + link: currentUrl, + item: items, + allowEmpty: true, + image, + author, + }; +}; + +export const route: Route = { + path: '/discover/:id?', + name: '发现', + url: 'top.aibase.com', + maintainers: ['nczitzk'], + handler, + example: '/aibase/discover', + parameters: { id: '发现分类,默认为空,即全部产品,可在对应发现分类页 URL 中找到' }, + description: `::: tip + 若订阅 [图片背景移除](https://top.aibase.com/discover/37-49),网址为 \`https://top.aibase.com/discover/37-49\`。截取 \`https://top.aibase.com/discover/\` 到末尾的部分 \`37-49\` 作为参数填入,此时路由为 [\`/aibase/discover/37-49\`](https://rsshub.app/aibase/discover/37-49)。 +::: + +
名称 | +{{ item.name }} | +
---|---|
标签 | ++ {{ each strToArray(item.tags) t }} + {{ t }}  + {{ /each }} + | +
类型 | ++ {{ if item.proctypename }} + {{ item.proctypename }} + {{ else }} + 无 + {{ /if }} + | +描述 | + {{ if item.desc }} + {{ item.desc }} + {{ else }} + 无 + {{ /if }} + +
需求人群 | +
+ {{ set list = strToArray(item.use) }}
+ {{ if list.length === 1 }}
+ {{ list[0] }}
+ {{ else }}
+ {{ each list l }}
+ |
+
使用场景示例 | +
+ {{ set list = strToArray(item.example) }}
+ {{ if list.length === 1 }}
+ {{ list[0] }}
+ {{ else }}
+ {{ each list l }}
+ |
+
产品特色 | +
+ {{ set list = strToArray(item.functions) }}
+ {{ if list.length === 1 }}
+ {{ list[0] }}
+ {{ else }}
+ {{ each list l }}
+ |
+
站点 | ++ {{ if item.url }} + + {{ item.url }} + + {{ else }} + 无 + {{ /if }} + | +
{{ intro }}+{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file diff --git a/lib/routes/ali213/zl.ts b/lib/routes/ali213/zl.ts new file mode 100644 index 00000000000000..47b9aaa3917002 --- /dev/null +++ b/lib/routes/ali213/zl.ts @@ -0,0 +1,226 @@ +import path from 'node:path'; + +import { type CheerioAPI, type Cheerio, type Element, load } from 'cheerio'; +import { type Context } from 'hono'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; + +import { art } from '@/utils/render'; +import cache from '@/utils/cache'; +import { getCurrentPath } from '@/utils/helpers'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +const __dirname = getCurrentPath(import.meta.url); + +export const handler = async (ctx: Context): Promise => { + const { category } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '1', 10); + + const rootUrl: string = 'https://www.ali213.net'; + const apiRootUrl: string = 'https://mp.ali213.net'; + const targetUrl: string = new URL(`/news/zl/${category ? (category.endsWith('/') ? category : `${category}/`) : ''}`, rootUrl).href; + const apiUrl: string = new URL('ajax/newslist', apiRootUrl).href; + + const response = await ofetch(apiUrl, { + query: { + type: 'new', + }, + }); + + const targetResponse = await ofetch(targetUrl); + const $: CheerioAPI = load(targetResponse); + const language: string = $('html').prop('lang') ?? 'zh'; + + let items: DataItem[] = JSON.parse(response.replace(/^\((.*)\)$/, '$1')) + .data.slice(0, limit) + .map((item): DataItem => { + const title: string = item.Title; + const description: string = art(path.join(__dirname, 'templates/description.art'), { + intro: item.GuideRead ?? '', + }); + const guid: string = `ali213-zl-${item.ID}`; + const image: string | undefined = item.PicPath ? `https:${item.PicPath}` : undefined; + + const author: DataItem['author'] = item.xiaobian; + + return { + title, + description, + pubDate: parseDate(item.addtime * 1000), + link: item.url ? `https:${item.url}` : undefined, + author, + guid, + id: guid, + content: { + html: description, + text: item.GuideRead ?? '', + }, + image, + banner: image, + language, + }; + }); + + items = ( + await Promise.all( + items.map((item) => { + if (!item.link && typeof item.link !== 'string') { + return item; + } + + return cache.tryGet(item.link, async (): Promise
(.+?)<\/p>/g, '
(.+?)<\/p>/g, '
${item.review}
+ `, + pubDate: new Date(item.date).toUTCString(), + })); + + const link = `https://appstare.net/data/app/comment/${appid}/${country}`; + + return { + title: 'App Comments', + appID: appid, + country, + item: items, + link, + allowEmpty: true, + }; +}; + +export const route: Route = { + path: '/comments/:country/:appid', + name: 'Comments', + url: 'appstare.net/', + example: '/appstare/comments/cn/989673964', + maintainers: ['zhixideyu'], + handler, + parameters: { + country: 'App Store country code, e.g., US, CN', + appid: 'Unique App Store application identifier (app id)', + }, + categories: ['program-update'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['appstare.net/'], + }, + ], + description: 'Retrieve only the comments of the app from the past 7 days.', +}; diff --git a/lib/routes/appstare/namespace.ts b/lib/routes/appstare/namespace.ts new file mode 100644 index 00000000000000..3d2809e68a9fe9 --- /dev/null +++ b/lib/routes/appstare/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'AppStare', + url: 'appstare.net', + lang: 'zh-CN', +}; diff --git a/lib/routes/appstore/namespace.ts b/lib/routes/appstore/namespace.ts index 4561b783496be9..cc4f08444621c0 100644 --- a/lib/routes/appstore/namespace.ts +++ b/lib/routes/appstore/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'App Store/Mac App Store', url: 'apps.apple.com', + lang: 'en', }; diff --git a/lib/routes/appstore/price.ts b/lib/routes/appstore/price.ts index f18fc5be06a487..996a274315deb7 100644 --- a/lib/routes/appstore/price.ts +++ b/lib/routes/appstore/price.ts @@ -50,7 +50,6 @@ async function handler(ctx) { title: unsupported, item: [{ title: unsupported }], }; - return; } let result = res.data.results.apps; diff --git a/lib/routes/appstorrent/namespace.ts b/lib/routes/appstorrent/namespace.ts index 3b5b12a004499b..f95fc97cb516bc 100644 --- a/lib/routes/appstorrent/namespace.ts +++ b/lib/routes/appstorrent/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AppsTorrent', url: 'appstorrent.ru', + lang: 'ru', }; diff --git a/lib/routes/aqara/namespace.ts b/lib/routes/aqara/namespace.ts index e14ff72843e459..d343eaea253e9f 100644 --- a/lib/routes/aqara/namespace.ts +++ b/lib/routes/aqara/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Aqara', url: 'aqara.com', + lang: 'zh-CN', }; diff --git a/lib/routes/aqara/post.ts b/lib/routes/aqara/post.ts index 8154f97399c2ed..6a803e6803c65c 100644 --- a/lib/routes/aqara/post.ts +++ b/lib/routes/aqara/post.ts @@ -31,7 +31,7 @@ async function handler(ctx) { if (filterMatches) { const filterRegion = filterMatches[1]; - const filterType = filterMatches[2] === 'tag' ? 'tags' : filterMatches[2] === 'category' ? 'categories' : filterMatches[2]; + const filterType = filterMatches[2] === 'tag' ? 'tags' : (filterMatches[2] === 'category' ? 'categories' : filterMatches[2]); const filterKeyword = decodeURI(filterMatches[3].split('/').pop()); const filterApiUrl = new URL(`${filterRegion}/${apiSlug}/${filterType}?search=${filterKeyword}`, rootUrl).href; diff --git a/lib/routes/aqara/region.ts b/lib/routes/aqara/region.ts index e3c20484d217d8..48174a0834bec3 100644 --- a/lib/routes/aqara/region.ts +++ b/lib/routes/aqara/region.ts @@ -14,5 +14,5 @@ function handler(ctx) { const { region = 'en', type = 'news' } = ctx.req.param(); const redirectTo = `/aqara/${region}/category/${types[type]}`; - ctx.redirect(redirectTo); + ctx.set('redirect', redirectTo); } diff --git a/lib/routes/aqicn/aqi.ts b/lib/routes/aqicn/aqi.ts index 62a5e04040596b..f5f94a129ed8f9 100644 --- a/lib/routes/aqicn/aqi.ts +++ b/lib/routes/aqicn/aqi.ts @@ -1,5 +1,6 @@ import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; +import { Route } from '@/types'; export const route: Route = { path: '/:city/:pollution?', diff --git a/lib/routes/aqicn/namespace.ts b/lib/routes/aqicn/namespace.ts index e370267b48c4ee..a0c29c99d6d400 100644 --- a/lib/routes/aqicn/namespace.ts +++ b/lib/routes/aqicn/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '空气质量', url: 'aqicn.org', + lang: 'zh-CN', }; diff --git a/lib/routes/arcteryx/namespace.ts b/lib/routes/arcteryx/namespace.ts index 0ada8bdaa86502..cc97c54bf81889 100644 --- a/lib/routes/arcteryx/namespace.ts +++ b/lib/routes/arcteryx/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Arcteryx', url: 'arcteryx.com', + lang: 'zh-CN', }; diff --git a/lib/routes/arcteryx/new-arrivals.ts b/lib/routes/arcteryx/new-arrivals.ts index 15c2fb755e0bf4..bc7e486091a518 100644 --- a/lib/routes/arcteryx/new-arrivals.ts +++ b/lib/routes/arcteryx/new-arrivals.ts @@ -30,19 +30,19 @@ export const route: Route = { handler, description: `Country - | United States | Canada | United Kingdom | - | ------------- | ------ | -------------- | - | us | ca | gb | +| United States | Canada | United Kingdom | +| ------------- | ------ | -------------- | +| us | ca | gb | gender - | male | female | - | ---- | ------ | - | mens | womens | +| male | female | +| ---- | ------ | +| mens | womens | - :::tip +::: tip Parameter \`country\` can be found within the url of \`Arcteryx\` website. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/arcteryx/outlet.ts b/lib/routes/arcteryx/outlet.ts index ac465a9f4cdf88..897cb35d4afbff 100644 --- a/lib/routes/arcteryx/outlet.ts +++ b/lib/routes/arcteryx/outlet.ts @@ -30,19 +30,19 @@ export const route: Route = { handler, description: `Country - | United States | Canada | United Kingdom | - | ------------- | ------ | -------------- | - | us | ca | gb | +| United States | Canada | United Kingdom | +| ------------- | ------ | -------------- | +| us | ca | gb | gender - | male | female | - | ---- | ------ | - | mens | womens | +| male | female | +| ---- | ------ | +| mens | womens | - :::tip +::: tip Parameter \`country\` can be found within the url of \`Arcteryx\` website. - :::`, +:::`, }; async function handler(ctx) { diff --git a/lib/routes/arknights/japan.ts b/lib/routes/arknights/japan.ts deleted file mode 100644 index 3e966b0b4c9c1e..00000000000000 --- a/lib/routes/arknights/japan.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Route } from '@/types'; -import got from '@/utils/got'; -import { parseDate } from '@/utils/parse-date'; - -export const route: Route = { - path: '/japan', - categories: ['game'], - example: '/arknights/japan', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['ak.arknights.jp/news', 'ak.arknights.jp/'], - }, - ], - name: 'アークナイツ (日服新闻)', - maintainers: ['ofyark'], - handler, - url: 'ak.arknights.jp/news', -}; - -async function handler() { - const response = await got({ - method: 'get', - url: 'https://www.arknights.jp:10014/news?lang=ja&limit=9&page=1', - }); - - const items = response.data.data.items; - const newsList = items.map((item) => ({ - title: item.title, - description: item.content[0].value, - pubDate: parseDate(item.publishedAt), - link: `https://www.arknights.jp/news/${item.id}`, - })); - - return { - title: 'アークナイツ', - link: 'https://www.arknights.jp/news', - description: 'アークナイツ ニュース', - language: 'ja', - item: newsList, - }; -} diff --git a/lib/routes/arknights/news.ts b/lib/routes/arknights/news.ts deleted file mode 100644 index 7fa268b1792f0a..00000000000000 --- a/lib/routes/arknights/news.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import { parseDate } from '@/utils/parse-date'; - -export const route: Route = { - path: '/news', - categories: ['game'], - example: '/arknights/news', - parameters: {}, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['ak-conf.hypergryph.com/*'], - }, - ], - name: '游戏公告与新闻', - maintainers: ['Astrian'], - handler, - url: 'ak-conf.hypergryph.com/*', -}; - -async function handler() { - const response = await got({ - method: 'get', - url: 'https://ak.hypergryph.com/news.html', - }); - - let newslist = response.data; - - const $ = load(newslist); - newslist = $('.articleItem > .articleItemLink'); - - newslist = await Promise.all( - newslist - .slice(0, 9) // limit article count to a single page - .map(async (index, item) => { - item = $(item); - const link = `https://ak.hypergryph.com${item.attr('href')}`; - const description = await cache.tryGet(link, async () => { - const result = await got(link); - const $ = load(result.data); - return $('.article-content').html(); - }); - return { - title: `[${item.find('.articleItemCate').first().text()}] ${item.find('.articleItemTitle').first().text()}`, - description, - link, - pubDate: parseDate(item.find('.articleItemDate').first().text()), - }; - }) - .get() - ); - - return { - title: '《明日方舟》游戏公告与新闻', - link: 'https://ak.hypergryph.com/news.html', - item: newslist, - }; -} diff --git a/lib/routes/artstation/namespace.ts b/lib/routes/artstation/namespace.ts index 48d714be2142b2..6967b625032b3b 100644 --- a/lib/routes/artstation/namespace.ts +++ b/lib/routes/artstation/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ArtStation', url: 'www.artstation.com', + lang: 'en', }; diff --git a/lib/routes/asiafruitchina/categories.ts b/lib/routes/asiafruitchina/categories.ts new file mode 100644 index 00000000000000..2e1c12584b6771 --- /dev/null +++ b/lib/routes/asiafruitchina/categories.ts @@ -0,0 +1,685 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +import { art } from '@/utils/render'; +import cache from '@/utils/cache'; +import { getCurrentPath } from '@/utils/helpers'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +import { type CheerioAPI, type Cheerio, type Element, load } from 'cheerio'; +import { type Context } from 'hono'; +import path from 'node:path'; + +const __dirname = getCurrentPath(import.meta.url); + +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 baseUrl: string = 'https://asiafruitchina.net'; + const targetUrl: string = new URL(`categories?gspx=${category}`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'zh-CN'; + + let items: DataItem[] = []; + + items = $('div.listBlocks ul li') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio发布者:{{ work.name }}
+评分:{{ work.rate_average_2dp }} | 评论数:{{ work.review_count }} | 总时长:{{ work.duration }} | 音频来源:{{ work.source_type }}
+价格:{{ work.price }} JPY | 销量:{{ work.dl_count }}
+分类:{{ work.category }}
+声优:{{ work.cv }}
\ No newline at end of file diff --git a/lib/routes/asmr-200/type.ts b/lib/routes/asmr-200/type.ts new file mode 100644 index 00000000000000..8036204afd1222 --- /dev/null +++ b/lib/routes/asmr-200/type.ts @@ -0,0 +1,96 @@ +export interface Result { + pagination: { + currentPage: number; + pageSize: number; + totalCount: number; + }; + works: Work[]; +} + +export interface Work { + age_category_string: string; + circle: { + id: number; + name: string; + source_id: string; + source_type: string; + }; + circle_id: number; + create_date: string; + dl_count: number; + duration: number; + has_subtitle: boolean; + id: number; + language_editions: { + display_order: number; + edition_id: number; + edition_type: string; + label: string; + lang: string; + workno: string; + }[]; + mainCoverUrl: string; + name: string; + nsfw: boolean; + original_workno: null | string; + other_language_editions_in_db: { + id: number; + is_original: boolean; + lang: string; + source_id: string; + source_type: string; + title: string; + }[]; + playlistStatus: any; + price: number; + rank: + | { + category: string; + rank: number; + rank_date: string; + term: string; + }[] + | null; + rate_average_2dp: number | number; + rate_count: number; + rate_count_detail: { + count: number; + ratio: number; + review_point: number; + }[]; + release: string; + review_count: number; + samCoverUrl: string; + source_id: string; + source_type: string; + source_url: string; + tags: { + i18n: any; + id: number; + name: string; + }[]; + category: string; + thumbnailCoverUrl: string; + title: string; + translation_info: { + child_worknos: string[]; + is_child: boolean; + is_original: boolean; + is_parent: boolean; + is_translation_agree: boolean; + is_translation_bonus_child: boolean; + is_volunteer: boolean; + lang: null | string; + original_workno: null | string; + parent_workno: null | string; + production_trade_price_rate: number; + translation_bonus_langs: string[]; + }; + userRating: null; + vas: { + id: string; + name: string; + }[]; + cv: string; + work_attributes: string; +} diff --git a/lib/routes/asus/bios.ts b/lib/routes/asus/bios.ts index e82587ce2dd4e3..2bb541dff082fe 100644 --- a/lib/routes/asus/bios.ts +++ b/lib/routes/asus/bios.ts @@ -2,26 +2,76 @@ import { Route } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import { art } from '@/utils/render'; import path from 'node:path'; +import cache from '@/utils/cache'; -const getProductID = async (model) => { - const searchAPI = `https://odinapi.asus.com.cn/recent-data/apiv2/SearchSuggestion?SystemCode=asus&WebsiteCode=cn&SearchKey=${model}&SearchType=ProductsAll&RowLimit=4&sitelang=cn`; - const response = await got(searchAPI); +const endPoints = { + zh: { + url: 'https://odinapi.asus.com.cn/', + lang: 'cn', + websiteCode: 'cn', + }, + en: { + url: 'https://odinapi.asus.com/', + lang: 'en', + websiteCode: 'global', + }, +}; - return { - productID: response.data.Result[0].Content[0].DataId, - url: response.data.Result[0].Content[0].Url, - }; +const getProductInfo = (model, language) => { + const currentEndpoint = endPoints[language] ?? endPoints.zh; + const { url, lang, websiteCode } = currentEndpoint; + + const searchAPI = `${url}recent-data/apiv2/SearchSuggestion?SystemCode=asus&WebsiteCode=${websiteCode}&SearchKey=${model}&SearchType=ProductsAll&RowLimit=4&sitelang=${lang}`; + + return cache.tryGet(`asus:bios:${model}:${language}`, async () => { + const response = await ofetch(searchAPI); + const product = response.Result[0].Content[0]; + + return { + productID: product.DataId, + hashId: product.HashId, + url: product.Url, + title: product.Title, + image: product.ImageURL, + m1Id: product.M1Id, + productLine: product.ProductLine, + }; + }) as Promise<{ + productID: string; + hashId: string; + url: string; + title: string; + image: string; + m1Id: string; + productLine: string; + }>; }; export const route: Route = { - path: '/bios/:model', + path: '/bios/:model/:lang?', categories: ['program-update'], - example: '/asus/bios/RT-AX88U', - parameters: { model: 'Model, can be found in product page' }, + example: '/asus/bios/RT-AX88U/zh', + parameters: { + model: 'Model, can be found in product page', + lang: { + description: 'Language, provide access routes for other parts of the world', + options: [ + { + label: 'Chinese', + value: 'zh', + }, + { + label: 'Global', + value: 'en', + }, + ], + default: 'en', + }, + }, features: { requireConfig: false, requirePuppeteer: false, @@ -32,36 +82,50 @@ export const route: Route = { }, radar: [ { - source: ['asus.com.cn/'], + source: [ + 'www.asus.com/displays-desktops/:productLine/:series/:model', + 'www.asus.com/laptops/:productLine/:series/:model', + 'www.asus.com/motherboards-components/:productLine/:series/:model', + 'www.asus.com/networking-iot-servers/:productLine/:series/:model', + 'www.asus.com/:region/displays-desktops/:productLine/:series/:model', + 'www.asus.com/:region/laptops/:productLine/:series/:model', + 'www.asus.com/:region/motherboards-components/:productLine/:series/:model', + 'www.asus.com/:region/networking-iot-servers/:productLine/:series/:model', + ], + target: '/bios/:model', }, ], name: 'BIOS', maintainers: ['Fatpandac'], handler, - url: 'asus.com.cn/', + url: 'www.asus.com', }; async function handler(ctx) { const model = ctx.req.param('model'); - const { productID, url } = await getProductID(model); - const biosAPI = `https://www.asus.com.cn/support/api/product.asmx/GetPDBIOS?website=cn&model=${model}&pdid=${productID}&sitelang=cn`; + const language = ctx.req.param('lang') ?? 'en'; + const productInfo = await getProductInfo(model, language); + const biosAPI = + language === 'zh' ? `https://www.asus.com.cn/support/api/product.asmx/GetPDBIOS?website=cn&model=${model}&sitelang=cn` : `https://www.asus.com/support/api/product.asmx/GetPDBIOS?website=global&model=${model}&sitelang=en`; - const response = await got(biosAPI); - const biosList = response.data.Result.Obj[0].Files; + const response = await ofetch(biosAPI); + const biosList = response.Result.Obj[0].Files; const items = biosList.map((item) => ({ title: item.Title, description: art(path.join(__dirname, 'templates/bios.art'), { item, + language, }), - guid: url + item.Version, + guid: productInfo.url + item.Version, pubDate: parseDate(item.ReleaseDate, 'YYYY/MM/DD'), - link: url, + link: productInfo.url, })); return { - title: `${model} BIOS`, - link: url, + title: `${productInfo.title} BIOS`, + link: productInfo.url, + image: productInfo.image, item: items, }; } diff --git a/lib/routes/asus/namespace.ts b/lib/routes/asus/namespace.ts index a8df4f68690cb2..5bfdf756afa789 100644 --- a/lib/routes/asus/namespace.ts +++ b/lib/routes/asus/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'ASUS', url: 'asus.com.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/asus/templates/bios.art b/lib/routes/asus/templates/bios.art index 08bcee2ea332c9..559dcc7571a330 100644 --- a/lib/routes/asus/templates/bios.art +++ b/lib/routes/asus/templates/bios.art @@ -1,6 +1,13 @@ -更新信息:
-{{@ item.Description}} -版本: {{item.Version}}
-大小: {{item.FileSize}}
-更新日期: {{item.ReleaseDate}}
- +{{ if language !== 'zh' }} +Changes:
+ {{@ item.Description}} +Version: {{item.Version}}
+Size: {{item.FileSize}}
+Download: {{ item.DownloadUrl.Global.split('/').pop().split('?')[0] }}
+{{ else }} +更新信息:
+ {{@ item.Description}} +版本: {{item.Version}}
+大小: {{item.FileSize}}
+ +{{ /if }} diff --git a/lib/routes/atcoder/contest.ts b/lib/routes/atcoder/contest.ts index ebaf26a377d833..7e29b3ff5f6c92 100644 --- a/lib/routes/atcoder/contest.ts +++ b/lib/routes/atcoder/contest.ts @@ -22,23 +22,23 @@ export const route: Route = { handler, description: `Rated Range - | ABC Class (Rated for \~1999) | ARC Class (Rated for \~2799) | AGC Class (Rated for \~9999) | - | ---------------------------- | ---------------------------- | ---------------------------- | - | 1 | 2 | 3 | +| ABC Class (Rated for \~1999) | ARC Class (Rated for \~2799) | AGC Class (Rated for \~9999) | +| ---------------------------- | ---------------------------- | ---------------------------- | +| 1 | 2 | 3 | Category - | All | AtCoder Typical Contest | PAST Archive | Unofficial(unrated) | - | --- | ----------------------- | ------------ | ------------------- | - | 0 | 6 | 50 | 101 | +| All | AtCoder Typical Contest | PAST Archive | Unofficial(unrated) | +| --- | ----------------------- | ------------ | ------------------- | +| 0 | 6 | 50 | 101 | - | JOI Archive | Sponsored Tournament | Sponsored Parallel(rated) | - | ----------- | -------------------- | ------------------------- | - | 200 | 1000 | 1001 | +| JOI Archive | Sponsored Tournament | Sponsored Parallel(rated) | +| ----------- | -------------------- | ------------------------- | +| 200 | 1000 | 1001 | - | Sponsored Parallel(unrated) | Optimization Contest | - | --------------------------- | -------------------- | - | 1002 | 1200 |`, +| Sponsored Parallel(unrated) | Optimization Contest | +| --------------------------- | -------------------- | +| 1002 | 1200 |`, }; async function handler(ctx) { diff --git a/lib/routes/atcoder/namespace.ts b/lib/routes/atcoder/namespace.ts index 0f4fa427f724df..cb177cc58be66f 100644 --- a/lib/routes/atcoder/namespace.ts +++ b/lib/routes/atcoder/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'AtCoder', url: 'atcoder.jp', + lang: 'en', }; diff --git a/lib/routes/atptour/namespace.ts b/lib/routes/atptour/namespace.ts index 5a0805bd67a7d5..0916b893b623c5 100644 --- a/lib/routes/atptour/namespace.ts +++ b/lib/routes/atptour/namespace.ts @@ -4,4 +4,5 @@ export const namespace: Namespace = { name: 'ATP Tour', url: 'www.atptour.com', description: "News from the official site of men's professional tennis.", + lang: 'en', }; diff --git a/lib/routes/auto-stats/index.ts b/lib/routes/auto-stats/index.ts index 87d4089fd5a16c..350de7136b7ab0 100644 --- a/lib/routes/auto-stats/index.ts +++ b/lib/routes/auto-stats/index.ts @@ -23,8 +23,8 @@ export const route: Route = { maintainers: ['nczitzk'], handler, description: `| 信息快递 | 工作动态 | 专题分析 | - | -------- | -------- | -------- | - | xxkd | gzdt | ztfx |`, +| -------- | -------- | -------- | +| xxkd | gzdt | ztfx |`, }; async function handler(ctx) { diff --git a/lib/routes/auto-stats/namespace.ts b/lib/routes/auto-stats/namespace.ts index 54adee1c7e09a9..247b60e68d57c0 100644 --- a/lib/routes/auto-stats/namespace.ts +++ b/lib/routes/auto-stats/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '中国汽车工业协会统计信息网', url: 'auto-stats.org.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/autocentre/index.ts b/lib/routes/autocentre/index.ts new file mode 100644 index 00000000000000..7a434f5a7f0fbb --- /dev/null +++ b/lib/routes/autocentre/index.ts @@ -0,0 +1,29 @@ +import { Data, Route } from '@/types'; +import parser from '@/utils/rss-parser'; + +export const route: Route = { + path: '/', + name: 'Автомобільний сайт N1 в Україні', + categories: ['new-media'], + maintainers: ['driversti'], + example: '/autocentre', + handler, +}; + +const createItem = (item) => ({ + title: item.title, + link: item.link, + description: item.contentSnippet, +}); + +async function handler(): Promise { + const feed = await parser.parseURL('https://www.autocentre.ua/rss'); + + return { + title: feed.title as string, + link: feed.link, + description: feed.description, + language: 'uk', + item: await Promise.all(feed.items.map((item) => createItem(item))), + }; +} diff --git a/lib/routes/autocentre/namespace.ts b/lib/routes/autocentre/namespace.ts new file mode 100644 index 00000000000000..b9db3ac3a9c2c3 --- /dev/null +++ b/lib/routes/autocentre/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Автоцентр.ua', + url: 'autocentre.ua', + description: 'Автоцентр.ua: автоновини - Автомобільний сайт N1 в Україні', + lang: 'ru', +}; diff --git a/lib/routes/baai/namespace.ts b/lib/routes/baai/namespace.ts index 409fd300b0aaba..3b7c640abe59ba 100644 --- a/lib/routes/baai/namespace.ts +++ b/lib/routes/baai/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '北京智源人工智能研究院', url: 'hub.baai.ac.cn', + lang: 'zh-CN', }; diff --git a/lib/routes/backlinko/namespace.ts b/lib/routes/backlinko/namespace.ts index 8ad09707cafa59..ec3016624fae29 100644 --- a/lib/routes/backlinko/namespace.ts +++ b/lib/routes/backlinko/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Backlinko', url: 'backlinko.com', + lang: 'en', }; diff --git a/lib/routes/bad/namespace.ts b/lib/routes/bad/namespace.ts index 56c2be9f943430..9338a21f419430 100644 --- a/lib/routes/bad/namespace.ts +++ b/lib/routes/bad/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Bad.news', url: 'bad.news', + lang: 'zh-CN', }; diff --git a/lib/routes/baidu/gushitong/index.ts b/lib/routes/baidu/gushitong/index.ts index c8bcd175e0a737..452131d24e17d8 100644 --- a/lib/routes/baidu/gushitong/index.ts +++ b/lib/routes/baidu/gushitong/index.ts @@ -1,4 +1,4 @@ -import { Route } from '@/types'; +import { Route, ViewType } from '@/types'; import { getCurrentPath } from '@/utils/helpers'; const __dirname = getCurrentPath(import.meta.url); @@ -13,7 +13,8 @@ const STATUS_MAP = { export const route: Route = { path: '/gushitong/index', - categories: ['finance'], + categories: ['finance', 'popular'], + view: ViewType.Notifications, example: '/baidu/gushitong/index', parameters: {}, features: { diff --git a/lib/routes/baidu/namespace.ts b/lib/routes/baidu/namespace.ts index 262deb052ecc02..d7447282ed7e8f 100644 --- a/lib/routes/baidu/namespace.ts +++ b/lib/routes/baidu/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '百度', url: 'www.baidu.com', + lang: 'zh-CN', }; diff --git a/lib/routes/baidu/search.ts b/lib/routes/baidu/search.ts index caf46980d62ddb..1d9bbdeb1478d2 100644 --- a/lib/routes/baidu/search.ts +++ b/lib/routes/baidu/search.ts @@ -42,15 +42,16 @@ async function handler(ctx) { const contentLeft = $('#content_left'); const containers = contentLeft.find('.c-container'); return containers - .map((i, el) => { + .toArray() + .map((el) => { const element = $(el); const link = element.find('h3 a').first().attr('href'); if (link && !visitedLinks.has(link)) { visitedLinks.add(link); const imgs = element .find('img') - .map((_j, _el) => $(_el).attr('src')) - .toArray(); + .toArray() + .map((_el) => $(_el).attr('src')); const description = element.find('.c-gap-top-small [class^="content-right_"]').first().text() || element.find('.c-row').first().text() || element.find('.cos-row').first().text(); return { title: element.find('h3').first().text(), @@ -61,7 +62,6 @@ async function handler(ctx) { } return null; }) - .toArray() .filter((e) => e?.link); }, config.cache.routeExpire, diff --git a/lib/routes/baidu/tieba/search.ts b/lib/routes/baidu/tieba/search.ts index 2b0e57c2db6c56..a8fc04f9578a76 100644 --- a/lib/routes/baidu/tieba/search.ts +++ b/lib/routes/baidu/tieba/search.ts @@ -27,11 +27,11 @@ export const route: Route = { maintainers: ['JimenezLi'], handler, description: `| 键 | 含义 | 接受的值 | 默认值 | - | ------------ | ---------------------------------------------------------- | ------------- | ------ | - | kw | 在名为 kw 的贴吧中搜索 | 任意名称 / 无 | 无 | - | only_thread | 只看主题帖,默认为 0 关闭 | 0/1 | 0 | - | rn | 返回条目的数量 | 1-20 | 20 | - | sm | 排序方式,0 为按时间顺序,1 为按时间倒序,2 为按相关性顺序 | 0/1/2 | 1 | +| ------------ | ---------------------------------------------------------- | ------------- | ------ | +| kw | 在名为 kw 的贴吧中搜索 | 任意名称 / 无 | 无 | +| only_thread | 只看主题帖,默认为 0 关闭 | 0/1 | 0 | +| rn | 返回条目的数量 | 1-20 | 20 | +| sm | 排序方式,0 为按时间顺序,1 为按时间倒序,2 为按相关性顺序 | 0/1/2 | 1 | 用例:\`/baidu/tieba/search/neuro/kw=neurosama&only_thread=1&sm=2\``, }; diff --git a/lib/routes/baidu/top.ts b/lib/routes/baidu/top.ts index 1da3cdc20e4c57..74feafc99d2c6b 100644 --- a/lib/routes/baidu/top.ts +++ b/lib/routes/baidu/top.ts @@ -24,8 +24,8 @@ export const route: Route = { maintainers: ['xyqfer'], handler, description: `| 热搜榜 | 小说榜 | 电影榜 | 电视剧榜 | 汽车榜 | 游戏榜 | - | -------- | ------ | ------ | -------- | ------ | ------ | - | realtime | novel | movie | teleplay | car | game |`, +| -------- | ------ | ------ | -------- | ------ | ------ | +| realtime | novel | movie | teleplay | car | game |`, }; async function handler(ctx) { diff --git a/lib/routes/baijing/index.ts b/lib/routes/baijing/index.ts new file mode 100644 index 00000000000000..79b2bda6b4ae4e --- /dev/null +++ b/lib/routes/baijing/index.ts @@ -0,0 +1,48 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/article', + categories: ['new-media'], + example: '/baijing/article', + url: 'www.baijing.cn/article/', + name: '资讯', + maintainers: ['p3psi-boo'], + handler, +}; + +async function handler() { + const apiUrl = 'https://www.baijing.cn/index/ajax/get_article/'; + const response = await ofetch(apiUrl); + const data = response.data.article_list; + + const list = data.map((item) => ({ + title: item.title, + link: `https://www.baijing.cn/article/${item.id}`, + author: item.user_info.user_name, + category: item.topic?.map((t) => t.title), + })); + + const items = await Promise.all( + list.map((item) => + cache.tryGet(item.link, async () => { + const response = await ofetch(item.link); + + const $ = load(response); + item.description = $('.content').html(); + item.pubDate = parseDate($('.timeago').text()); + + return item; + }) + ) + ); + + return { + title: '白鲸出海 - 资讯', + link: 'https://www.baijing.cn/article/', + item: items, + }; +} diff --git a/lib/routes/baijing/namespace.ts b/lib/routes/baijing/namespace.ts new file mode 100644 index 00000000000000..57d69294cd4383 --- /dev/null +++ b/lib/routes/baijing/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '白鲸出海', + url: 'baijing.cn', + description: '白鲸出海', + lang: 'zh-CN', +}; diff --git a/lib/routes/bandcamp/namespace.ts b/lib/routes/bandcamp/namespace.ts index 70a481f475a0fb..dc244d34eb8966 100644 --- a/lib/routes/bandcamp/namespace.ts +++ b/lib/routes/bandcamp/namespace.ts @@ -3,4 +3,5 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'Bandcamp', url: 'bandcamp.com', + lang: 'en', }; diff --git a/lib/routes/bangumi/moe/index.ts b/lib/routes/bangumi.moe/index.ts similarity index 94% rename from lib/routes/bangumi/moe/index.ts rename to lib/routes/bangumi.moe/index.ts index 1dea1500b281dc..e8a3667a8a576a 100644 --- a/lib/routes/bangumi/moe/index.ts +++ b/lib/routes/bangumi.moe/index.ts @@ -5,21 +5,22 @@ import got from '@/utils/got'; import { parseDate } from '@/utils/parse-date'; export const route: Route = { - path: '/moe/*', + path: '/*', + categories: ['anime'], radar: [ { source: ['bangumi.moe/'], - target: '/moe', }, ], - name: 'Unknown', - maintainers: [], + name: 'Latest', + example: '/bangumi.moe', + maintainers: ['nczitzk'], handler, url: 'bangumi.moe/', }; async function handler(ctx) { - const isLatest = getSubPath(ctx) === '/moe'; + const isLatest = getSubPath(ctx) === '/'; const rootUrl = 'https://bangumi.moe'; let response; diff --git a/lib/routes/bangumi.moe/namespace.ts b/lib/routes/bangumi.moe/namespace.ts new file mode 100644 index 00000000000000..697c3a2b4f4b4b --- /dev/null +++ b/lib/routes/bangumi.moe/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '萌番组', + url: 'bangumi.online', + lang: 'zh-CN', +}; diff --git a/lib/routes/bangumi/namespace.ts b/lib/routes/bangumi.online/namespace.ts similarity index 72% rename from lib/routes/bangumi/namespace.ts rename to lib/routes/bangumi.online/namespace.ts index c2670f8bca306f..fd31a2bab5074f 100644 --- a/lib/routes/bangumi/namespace.ts +++ b/lib/routes/bangumi.online/namespace.ts @@ -2,5 +2,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: 'アニメ新番組', - url: 'bangumi.moe', + url: 'bangumi.online', + lang: 'ja', }; diff --git a/lib/routes/bangumi/online/online.ts b/lib/routes/bangumi.online/online.ts similarity index 91% rename from lib/routes/bangumi/online/online.ts rename to lib/routes/bangumi.online/online.ts index 02dcc058b404f9..9312fc8b5c5f8d 100644 --- a/lib/routes/bangumi/online/online.ts +++ b/lib/routes/bangumi.online/online.ts @@ -8,9 +8,9 @@ import { parseDate } from '@/utils/parse-date'; import path from 'node:path'; export const route: Route = { - path: '/online', + path: '/', categories: ['anime'], - example: '/bangumi/online', + example: '/bangumi.online', parameters: {}, features: { requireConfig: false, @@ -40,7 +40,7 @@ async function handler() { const items = list.map((item) => ({ title: `${item.title.zh ?? item.title.ja} - 第 ${item.volume} 集`, - description: art(path.join(__dirname, '../templates/online/image.art'), { + description: art(path.join(__dirname, 'templates/image.art'), { src: `https:${item.cover}`, alt: `${item.title_zh} - 第 ${item.volume} 集`, }), diff --git a/lib/routes/bangumi/templates/online/image.art b/lib/routes/bangumi.online/templates/image.art similarity index 100% rename from lib/routes/bangumi/templates/online/image.art rename to lib/routes/bangumi.online/templates/image.art diff --git a/lib/routes/bangumi/tv/calendar/_base.ts b/lib/routes/bangumi.tv/calendar/_base.ts similarity index 100% rename from lib/routes/bangumi/tv/calendar/_base.ts rename to lib/routes/bangumi.tv/calendar/_base.ts diff --git a/lib/routes/bangumi/tv/calendar/today.ts b/lib/routes/bangumi.tv/calendar/today.ts similarity index 89% rename from lib/routes/bangumi/tv/calendar/today.ts rename to lib/routes/bangumi.tv/calendar/today.ts index a82807c2a95169..2f12eaa9cae9b6 100644 --- a/lib/routes/bangumi/tv/calendar/today.ts +++ b/lib/routes/bangumi.tv/calendar/today.ts @@ -8,9 +8,9 @@ import { art } from '@/utils/render'; import path from 'node:path'; export const route: Route = { - path: '/tv/calendar/today', + path: '/calendar/today', categories: ['anime'], - example: '/bangumi/tv/calendar/today', + example: '/bangumi.tv/calendar/today', parameters: {}, features: { requireConfig: false, @@ -42,10 +42,10 @@ async function handler() { const todayList = list.find((l) => l.weekday.id % 7 === day); const todayBgmId = new Set(todayList.items.map((t) => t.id.toString())); - const images = todayList.items.reduce((p, c) => { - p[c.id] = (c.images || {}).large; - return p; - }, {}); + const images: { [key: string]: string } = {}; + for (const item of todayList.items) { + images[item.id] = (item.images || {}).large; + } const todayBgm = data.items.filter((d) => todayBgmId.has(d.bgmId)); for (const bgm of todayBgm) { bgm.image = images[bgm.bgmId]; @@ -65,7 +65,7 @@ async function handler() { const link = `https://bangumi.tv/subject/${bgm.bgmId}`; const id = `${link}#${new Intl.DateTimeFormat('zh-CN').format(updated)}`; - const html = art(path.resolve(__dirname, '../../templates/tv/today.art'), { + const html = art(path.join(__dirname, '../templates/today.art'), { bgm, siteMeta, }); diff --git a/lib/routes/bangumi/tv/group/reply.ts b/lib/routes/bangumi.tv/group/reply.ts similarity index 94% rename from lib/routes/bangumi/tv/group/reply.ts rename to lib/routes/bangumi.tv/group/reply.ts index 6e4cd264f509cc..33a25c0dab1a4c 100644 --- a/lib/routes/bangumi/tv/group/reply.ts +++ b/lib/routes/bangumi.tv/group/reply.ts @@ -1,13 +1,13 @@ import { Route } from '@/types'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; export const route: Route = { - path: '/tv/topic/:id', + path: '/topic/:id', categories: ['anime'], - example: '/bangumi/tv/topic/367032', + example: '/bangumi.tv/topic/367032', parameters: { id: '话题 id, 在话题页面地址栏查看' }, features: { requireConfig: false, @@ -31,7 +31,7 @@ async function handler(ctx) { // bangumi.tv未提供获取小组话题的API,因此仍需要通过抓取网页来获取 const topicID = ctx.req.param('id'); const link = `https://bgm.tv/group/topic/${topicID}`; - const { data: html } = await got(link); + const html = await ofetch(link); const $ = load(html); const title = $('#pageHeader h1').text(); const latestReplies = $('.row_reply') diff --git a/lib/routes/bangumi/tv/group/topic.ts b/lib/routes/bangumi.tv/group/topic.ts similarity index 54% rename from lib/routes/bangumi/tv/group/topic.ts rename to lib/routes/bangumi.tv/group/topic.ts index 95f9b37aada29d..2ba94387fc73be 100644 --- a/lib/routes/bangumi/tv/group/topic.ts +++ b/lib/routes/bangumi.tv/group/topic.ts @@ -1,14 +1,14 @@ import { Route } from '@/types'; import cache from '@/utils/cache'; -import got from '@/utils/got'; +import ofetch from '@/utils/ofetch'; import { load } from 'cheerio'; import { parseDate } from '@/utils/parse-date'; -const base_url = 'https://bgm.tv'; +const baseUrl = 'https://bgm.tv'; export const route: Route = { - path: '/tv/group/:id', + path: '/group/:id', categories: ['anime'], - example: '/bangumi/tv/group/boring', + example: '/bangumi.tv/group/boring', parameters: { id: '小组 id, 在小组页面地址栏查看' }, features: { requireConfig: false, @@ -30,29 +30,29 @@ export const route: Route = { async function handler(ctx) { const groupID = ctx.req.param('id'); - const link = `${base_url}/group/${groupID}/forum`; - const { data: html } = await got(link); + const link = `${baseUrl}/group/${groupID}/forum`; + const html = await ofetch(link); const $ = load(html); const title = 'Bangumi - ' + $('.SecondaryNavTitle').text(); const items = await Promise.all( $('.topic_list .topic') .toArray() - .map(async (elem) => { - const link = new URL($('.subject a', elem).attr('href'), base_url).href; - const fullText = await cache.tryGet(link, async () => { - const { data: html } = await got(link); + .map((elem) => { + const link = new URL($('.subject a', elem).attr('href'), baseUrl).href; + return cache.tryGet(link, async () => { + const html = await ofetch(link); const $ = load(html); - return $('.postTopic .topic_content').html(); + const fullText = $('.postTopic .topic_content').html(); + const summary = 'Reply: ' + $('.posts', elem).text(); + return { + link, + title: $('.subject a', elem).attr('title'), + pubDate: parseDate($('.lastpost .time', elem).text()), + description: fullText ? summary + '${item.ImageContent.Description}
`;
+ }
+ return {
+ title: item.ImageContent.Title,
+ description,
+ link: `${apiUrl}${item.ImageContent.BackstageUrl}`,
+ author: item.ImageContent.Copyright,
+ pubDate: timezone(parseDate(ssd, 'YYYYMMDD_HHmm'), 0),
+ };
+ });
return {
title: 'Bing每日壁纸',
- link: 'https://cn.bing.com/',
- item: data.images.map((item) => ({
- title: item.copyright,
- description: ` 和 中无用的内容
+ newsContent.find('p, span, strong').each(function () {
+ const element = content(this);
+ const text = element.text().trim();
+
+ // 删除没有有用文本的元素,防止空元素被保留
+ if (text === '') {
+ element.remove();
+ } else {
+ // 去除多余的嵌套标签,但保留其内容
+ element.replaceWith(text);
+ }
+ });
+
+ // 清理后的内容转换为文本
+ const cleanedDescription = newsContent.text().trim();
+
+ // 提取并格式化发布时间
+ item.description = cleanedDescription;
+ item.pubDate = timezone(parseDate(content('.info').text().replace('发布时间:', '').trim()), +8);
+
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: `北京邮电大学教务处 - ${pageTitle}`,
+ link: currentUrl,
+ item: items,
+ };
+}
diff --git a/lib/routes/bupt/namespace.ts b/lib/routes/bupt/namespace.ts
index 8d7475341a2aa6..6a0dca8e8498db 100644
--- a/lib/routes/bupt/namespace.ts
+++ b/lib/routes/bupt/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '北京邮电大学',
url: 'bupt.edu.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/byau/namespace.ts b/lib/routes/byau/namespace.ts
index 9b05692137cca1..e256a5556e3bee 100644
--- a/lib/routes/byau/namespace.ts
+++ b/lib/routes/byau/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '黑龙江八一农垦大学',
url: 'byau.edu.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/byau/xinwen/index.ts b/lib/routes/byau/xinwen/index.ts
index 4fec6eee071b26..b6dc00800f9c8b 100644
--- a/lib/routes/byau/xinwen/index.ts
+++ b/lib/routes/byau/xinwen/index.ts
@@ -21,8 +21,8 @@ export const route: Route = {
handler,
url: 'xinwen.byau.edu.cn',
description: `| 学校要闻 | 校园动态 |
- | ---- | ----------- |
- | 3674 | 3676 |`,
+| ---- | ----------- |
+| 3674 | 3676 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/byteclicks/namespace.ts b/lib/routes/byteclicks/namespace.ts
index f6932376ef668c..159f94cf99d87a 100644
--- a/lib/routes/byteclicks/namespace.ts
+++ b/lib/routes/byteclicks/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '字节点击',
url: 'byteclicks.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/bytes/namespace.ts b/lib/routes/bytes/namespace.ts
index 99b9f6beb30dfb..7b040e171a07eb 100644
--- a/lib/routes/bytes/namespace.ts
+++ b/lib/routes/bytes/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'ui.dev',
url: 'bytes.dev',
+ lang: 'en',
};
diff --git a/lib/routes/c114/namespace.ts b/lib/routes/c114/namespace.ts
index 3c44a5c483146f..dd5c3be2afd3ae 100644
--- a/lib/routes/c114/namespace.ts
+++ b/lib/routes/c114/namespace.ts
@@ -3,4 +3,7 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'C114 通信网',
url: 'c114.com.cn',
+ categories: ['new-media'],
+ description: '',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/c114/roll.ts b/lib/routes/c114/roll.ts
index ae22d3c79884fa..2e4835cad7a7b1 100644
--- a/lib/routes/c114/roll.ts
+++ b/lib/routes/c114/roll.ts
@@ -1,4 +1,5 @@
import { Route } from '@/types';
+
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
@@ -6,78 +7,106 @@ import timezone from '@/utils/timezone';
import { parseDate } from '@/utils/parse-date';
import iconv from 'iconv-lite';
-export const route: Route = {
- path: '/roll',
- categories: ['new-media'],
- example: '/c114/roll',
- parameters: {},
- features: {
- requireConfig: false,
- requirePuppeteer: false,
- antiCrawler: false,
- supportBT: false,
- supportPodcast: false,
- supportScihub: false,
- },
- radar: [
- {
- source: ['c114.com.cn/news/roll.asp', 'c114.com.cn/'],
- },
- ],
- name: '滚动新闻',
- maintainers: ['nczitzk'],
- handler,
- url: 'c114.com.cn/news/roll.asp',
-};
+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;
-async function handler(ctx) {
const rootUrl = 'https://www.c114.com.cn';
- const currentUrl = `${rootUrl}/news/roll.asp`;
+ const currentUrl = new URL(`news/roll.asp${original === 'true' ? `?o=true` : ''}`, rootUrl).href;
- const response = await got({
- method: 'get',
- url: currentUrl,
+ const { data: response } = await got(currentUrl, {
responseType: 'buffer',
});
- const $ = load(iconv.decode(response.data, 'gbk'));
+ const $ = load(iconv.decode(response, 'gbk'));
+
+ const language = $('html').prop('lang');
- let items = $('.new_list_c h6 a')
- .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 50)
+ let items = $('div.new_list_c')
+ .slice(0, limit)
.toArray()
.map((item) => {
item = $(item);
return {
- title: item.text(),
- link: item.attr('href'),
+ title: item.find('h6 a').text(),
+ pubDate: timezone(parseDate(item.find('div.new_list_time').text(), ['HH:mm', 'M/D']), +8),
+ link: new URL(item.find('h6 a').prop('href'), rootUrl).href,
+ author: item.find('div.new_list_author').text().trim(),
+ language,
};
});
items = await Promise.all(
items.map((item) =>
cache.tryGet(item.link, async () => {
- const detailResponse = await got({
- method: 'get',
- url: item.link,
+ const { data: detailResponse } = await got(item.link, {
responseType: 'buffer',
});
- const content = load(iconv.decode(detailResponse.data, 'gbk'));
+ const $$ = load(iconv.decode(detailResponse, 'gbk'));
- item.description = content('.text').html();
- item.author = content('.author').first().text().replace('C114通信网 ', '');
- item.pubDate = timezone(parseDate(content('.r_time').text()), +8);
- item.category = content('meta[name="keywords"]').attr('content').split(',');
+ const title = $$('h1').text();
+ const description = $$('div.text').html();
+
+ item.title = title;
+ item.description = description;
+ item.pubDate = timezone(parseDate($$('div.r_time').text(), 'YYYY/M/D HH:mm'), +8);
+ item.author = $$('div.author').first().text().trim();
+ item.content = {
+ html: description,
+ text: $$('.text').text(),
+ };
+ item.language = language;
return item;
})
)
);
+ const image = new URL($('div.top2-1 a img').prop('src'), rootUrl).href;
+
return {
title: $('title').text(),
+ description: $('meta[name="description"]').prop('content'),
link: currentUrl,
item: items,
+ allowEmpty: true,
+ image,
+ author: $('p.top1-1-1 a').first().text(),
+ language,
};
-}
+};
+
+export const route: Route = {
+ path: '/roll/:original?',
+ name: '滚动资讯',
+ url: 'c114.com.cn',
+ maintainers: ['nczitzk'],
+ handler,
+ example: '/c114/roll',
+ parameters: { original: '只看原创,可选 true 和 false,默认为 false' },
+ description: '',
+ categories: ['new-media'],
+
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['c114.com.cn/news/roll.asp'],
+ target: (_, url) => {
+ url = new URL(url);
+ const original = url.searchParams.get('o');
+
+ return `/roll${original ? `/${original}` : ''}`;
+ },
+ },
+ ],
+};
diff --git a/lib/routes/caai/namespace.ts b/lib/routes/caai/namespace.ts
index aa50eac2ef5bec..bf586f31ff981f 100644
--- a/lib/routes/caai/namespace.ts
+++ b/lib/routes/caai/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国人工智能学会',
url: 'caai.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/caam/namespace.ts b/lib/routes/caam/namespace.ts
index 237b6afc0eb3b3..fe756ac0a46881 100644
--- a/lib/routes/caam/namespace.ts
+++ b/lib/routes/caam/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国汽车工业协会',
url: 'caam.org.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/caareviews/namespace.ts b/lib/routes/caareviews/namespace.ts
index 8aa1f4c722e75a..5ebfc5382e2d82 100644
--- a/lib/routes/caareviews/namespace.ts
+++ b/lib/routes/caareviews/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'caa.reviews',
url: 'caareviews.org',
+ lang: 'en',
};
diff --git a/lib/routes/cags/edu/index.ts b/lib/routes/cags/edu/index.ts
new file mode 100644
index 00000000000000..f5e71ed6d6fb7e
--- /dev/null
+++ b/lib/routes/cags/edu/index.ts
@@ -0,0 +1,84 @@
+import ofetch from '@/utils/ofetch';
+import { Route } from '@/types';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+
+const host = 'https://edu.cags.ac.cn';
+
+const titles = {
+ tzgg: '通知公告',
+ ywjx: '要闻简讯',
+ zs_bss: '博士生招生',
+ zs_sss: '硕士生招生',
+ zs_dxsxly: '大学生夏令营',
+};
+
+export const route: Route = {
+ path: '/edu/:category',
+ categories: ['university'],
+ example: '/cags/edu/tzgg',
+ parameters: {
+ category: '通知频道,可选 tzgg/ywjx/zs_bss/zs_sss/zs_dxsxly',
+ },
+ features: {
+ antiCrawler: false,
+ requireConfig: false,
+ requirePuppeteer: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ name: '研究生院',
+ maintainers: ['Chikit-L'],
+ radar: [
+ {
+ source: ['edu.cags.ac.cn/'],
+ },
+ ],
+ handler,
+ description: `
+| 通知公告 | 要闻简讯 | 博士生招生 | 硕士生招生 | 大学生夏令营 |
+| -------- | -------- | ---------- | ---------- | ------------ |
+| tzgg | ywjx | zs_bss | zs_sss | zs_dxsxly |
+`,
+};
+
+async function handler(ctx) {
+ const category = ctx.req.param('category');
+ const title = titles[category];
+
+ if (!title) {
+ throw new Error(`Invalid category: ${category}`);
+ }
+
+ const API_URL = `${host}/api/cms/cmsNews/pageByCmsNavBarId/${category}/1/10/0`;
+ const response = await ofetch(API_URL);
+ const data = response.data;
+
+ const items = data.map((item) => {
+ const id = item.id;
+ const title = item.title;
+
+ let pubDate = null;
+ if (item.publishDate) {
+ pubDate = parseDate(item.publishDate, 'YYYY-MM-DD');
+ pubDate = timezone(pubDate, 8);
+ }
+
+ const link = `${host}/#/dky/view/id=${id}/barId=${category}`;
+
+ return {
+ title,
+ description: item.introduction,
+ link,
+ guid: link,
+ pubDate,
+ };
+ });
+
+ return {
+ title,
+ link: `${host}/#/dky/list/barId=${category}/cmsNavCategory=1`,
+ item: items,
+ };
+}
diff --git a/lib/routes/cags/namespace.ts b/lib/routes/cags/namespace.ts
new file mode 100644
index 00000000000000..abb34c06bfc9fb
--- /dev/null
+++ b/lib/routes/cags/namespace.ts
@@ -0,0 +1,9 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Chinese Academy of Geological Sciences',
+ url: 'cags.cgs.gov.cn',
+ zh: {
+ name: '中国地质科学院',
+ },
+};
diff --git a/lib/routes/cahkms/index.ts b/lib/routes/cahkms/index.ts
index 1e6f24b6a3057a..7f9d8f3fa3078d 100644
--- a/lib/routes/cahkms/index.ts
+++ b/lib/routes/cahkms/index.ts
@@ -46,12 +46,12 @@ export const route: Route = {
handler,
url: 'cahkms.org/',
description: `| 关于我们 | 港澳新闻 | 重要新闻 | 顾问点评、会员观点 | 专题汇总 |
- | -------- | -------- | -------- | ------------------ | -------- |
- | 01 | 02 | 03 | 04 | 05 |
+| -------- | -------- | -------- | ------------------ | -------- |
+| 01 | 02 | 03 | 04 | 05 |
- | 港澳时评 | 图片新闻 | 视频中心 | 港澳研究 | 最新书讯 | 研究资讯 |
- | -------- | -------- | -------- | -------- | -------- | -------- |
- | 06 | 07 | 08 | 09 | 10 | 11 |`,
+| 港澳时评 | 图片新闻 | 视频中心 | 港澳研究 | 最新书讯 | 研究资讯 |
+| -------- | -------- | -------- | -------- | -------- | -------- |
+| 06 | 07 | 08 | 09 | 10 | 11 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cahkms/namespace.ts b/lib/routes/cahkms/namespace.ts
index 8d51e2b858146e..941ab11a9aca19 100644
--- a/lib/routes/cahkms/namespace.ts
+++ b/lib/routes/cahkms/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '全国港澳研究会',
url: 'cahkms.org',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/caijing/namespace.ts b/lib/routes/caijing/namespace.ts
index 6f2acf580b363c..4483b85f997c7c 100644
--- a/lib/routes/caijing/namespace.ts
+++ b/lib/routes/caijing/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '财经网',
url: 'roll.caijing.com.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/caixin/article.ts b/lib/routes/caixin/article.ts
index 9ab71ea06953fb..1fd50c39203a2d 100644
--- a/lib/routes/caixin/article.ts
+++ b/lib/routes/caixin/article.ts
@@ -42,7 +42,7 @@ async function handler() {
audio_image_url: item.audio_image_url,
}));
- const items = await Promise.all(list.map((item) => parseArticle(item, cache.tryGet)));
+ const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => parseArticle(item))));
return {
title: '财新网 - 首页',
diff --git a/lib/routes/caixin/blog.ts b/lib/routes/caixin/blog.ts
index 4c5b6849ef0795..90997606aca5bc 100644
--- a/lib/routes/caixin/blog.ts
+++ b/lib/routes/caixin/blog.ts
@@ -67,8 +67,7 @@ async function handler(ctx) {
pubDate: parseDate(item.publishTime, 'x'),
}));
- const items = await Promise.all(posts.map((item) => parseBlogArticle(item, cache.tryGet)));
-
+ const items = await Promise.all(posts.map((item) => cache.tryGet(item.link, () => parseBlogArticle(item))));
return {
title: `财新博客 - ${authorName}`,
link,
@@ -90,7 +89,7 @@ async function handler(ctx) {
link: item.postUrl.replace('http://', 'https://'),
pubDate: parseDate(item.publishTime, 'x'),
}));
- const items = await Promise.all(posts.map((item) => parseBlogArticle(item, cache.tryGet)));
+ const items = await Promise.all(posts.map((item) => cache.tryGet(item.link, () => parseBlogArticle(item))));
return {
title: `财新博客 - 全部`,
diff --git a/lib/routes/caixin/category.ts b/lib/routes/caixin/category.ts
index 60f17b21aad69f..134c9a162b3b49 100644
--- a/lib/routes/caixin/category.ts
+++ b/lib/routes/caixin/category.ts
@@ -26,21 +26,21 @@ export const route: Route = {
handler,
description: `Column 列表:
- | 经济 | 金融 | 政经 | 环科 | 世界 | 观点网 | 文化 | 周刊 |
- | ------- | ------- | ----- | ------- | ------------- | ------- | ------- | ------ |
- | economy | finance | china | science | international | opinion | culture | weekly |
+| 经济 | 金融 | 政经 | 环科 | 世界 | 观点网 | 文化 | 周刊 |
+| ------- | ------- | ----- | ------- | ------------- | ------- | ------- | ------ |
+| economy | finance | china | science | international | opinion | culture | weekly |
以金融板块为例的 category 列表:(其余 column 以类似方式寻找)
- | 监管 | 银行 | 证券基金 | 信托保险 | 投资 | 创新 | 市场 |
- | ---------- | ---- | -------- | ---------------- | ---------- | ---------- | ------ |
- | regulation | bank | stock | insurance\_trust | investment | innovation | market |
+| 监管 | 银行 | 证券基金 | 信托保险 | 投资 | 创新 | 市场 |
+| ---------- | ---- | -------- | ---------------- | ---------- | ---------- | ------ |
+| regulation | bank | stock | insurance\_trust | investment | innovation | market |
Category 列表:
- | 封面报道 | 开卷 | 社论 | 时事 | 编辑寄语 | 经济 | 金融 | 商业 | 环境与科技 | 民生 | 副刊 |
- | ---------- | ----- | --------- | ---------------- | ------------ | ------- | ------- | -------- | ----------------------- | ------- | ------ |
- | coverstory | first | editorial | current\_affairs | editor\_desk | economy | finance | business | environment\_technology | cwcivil | column |`,
+| 封面报道 | 开卷 | 社论 | 时事 | 编辑寄语 | 经济 | 金融 | 商业 | 环境与科技 | 民生 | 副刊 |
+| ---------- | ----- | --------- | ---------------- | ------------ | ------- | ------- | -------- | ----------------------- | ------- | ------ |
+| coverstory | first | editorial | current\_affairs | editor\_desk | economy | finance | business | environment\_technology | cwcivil | column |`,
};
async function handler(ctx) {
@@ -83,7 +83,7 @@ async function handler(ctx) {
audio_image_url: item.pict.imgs[0].url,
}));
- const items = await Promise.all(list.map((item) => parseArticle(item, cache.tryGet)));
+ const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => parseArticle(item))));
return {
title,
diff --git a/lib/routes/caixin/latest.ts b/lib/routes/caixin/latest.ts
index 599e0d8ad2198f..da585b75c2e293 100644
--- a/lib/routes/caixin/latest.ts
+++ b/lib/routes/caixin/latest.ts
@@ -1,16 +1,14 @@
-import { Route } from '@/types';
-import { getCurrentPath } from '@/utils/helpers';
-const __dirname = getCurrentPath(import.meta.url);
+import { Route, ViewType } from '@/types';
+import { getFulltext } from './utils-fulltext';
import cache from '@/utils/cache';
import got from '@/utils/got';
-import { load } from 'cheerio';
-import { art } from '@/utils/render';
-import path from 'node:path';
+import { parseArticle } from './utils';
export const route: Route = {
path: '/latest',
- categories: ['traditional-media'],
+ categories: ['traditional-media', 'popular'],
+ view: ViewType.Articles,
example: '/caixin/latest',
parameters: {},
features: {
@@ -30,10 +28,10 @@ export const route: Route = {
maintainers: ['tpnonthealps'],
handler,
url: 'caixin.com/',
- description: `说明:此 RSS feed 会自动抓取财新网的最新文章,但不包含 FM 及视频内容。`,
+ description: `说明:此 RSS feed 会自动抓取财新网的最新文章,但不包含 FM 及视频内容。订阅用户可根据文档设置环境变量后,在url传入\`fulltext=\`以解锁全文。`,
};
-async function handler() {
+async function handler(ctx) {
const { data } = await got('https://gateway.caixin.com/api/dataplatform/scroll/index');
const list = data.data.articleList
@@ -48,21 +46,20 @@ async function handler() {
const rss = await Promise.all(
list.map((item) =>
cache.tryGet(`caixin:latest:${item.link}`, async () => {
- const entry_r = await got(item.link);
- const $ = load(entry_r.data);
-
// desc
- const desc = art(path.join(__dirname, 'templates/article.art'), {
- item,
- $,
- });
+ const desc = await parseArticle(item);
- item.description = desc;
+ if (ctx.req.query('fulltext') === 'true') {
+ const authorizedFullText = await getFulltext(item.link);
+ item.description = authorizedFullText === '' ? desc.description : authorizedFullText;
+ } else {
+ item.description = desc.description;
+ }
// prevent cache coliision with /caixin/article and /caixin/:column/:category
// since those have podcasts
item.guid = `caixin:latest:${item.link}`;
- return item;
+ return { ...desc, ...item };
})
)
);
diff --git a/lib/routes/caixin/namespace.ts b/lib/routes/caixin/namespace.ts
index 8099f11d8d4ef5..b00547f4e02ca1 100644
--- a/lib/routes/caixin/namespace.ts
+++ b/lib/routes/caixin/namespace.ts
@@ -3,5 +3,6 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '财新博客',
url: 'caixin.com',
- description: `> 网站部分内容需要付费订阅,RSS 仅做更新提醒,不含付费内容。`,
+ description: `> 网站部分内容需要付费订阅,RSS 仅做更新提醒,不含付费内容。若需要得到付费内容全文,请使用订阅账户在手机网页版登录,然后设置\`CAIXIN_COOKIE\`为至少包含cookie中的以下字段: \`SA_USER_UID\`, \`SA_USER_UNIT\`, \`SA_USER_DEVICE_TYPE\`, \`USER_LOGIN_CODE\``,
+ lang: 'zh-CN',
};
diff --git a/lib/routes/caixin/utils-fulltext.ts b/lib/routes/caixin/utils-fulltext.ts
new file mode 100644
index 00000000000000..c866784508e529
--- /dev/null
+++ b/lib/routes/caixin/utils-fulltext.ts
@@ -0,0 +1,51 @@
+import crypto from 'crypto';
+import { hextob64, KJUR } from 'jsrsasign';
+import ofetch from '@/utils/ofetch';
+import { config } from '@/config';
+
+// The following constant is extracted from this script: https://file.caixin.com/pkg/cx-pay-layer/js/wap.js?v=5.15.421933 . It is believed to contain no sensitive information.
+// Refer to this discussion for further explanation: https://github.com/DIYgod/RSSHub/pull/17231
+const rsaPrivateKey =
+ '-----BEGIN PRIVATE KEY-----MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCLci8q2u3NGFyFlMUwjCP91PsvGjHdRAq9fmqZLxvue+n+RhzNxnKKYOv35pLgFKWXsGq2TV+5Xrv6xZgNx36IUkqbmrO+eCa8NFmti04wvMfG3DCNdKA7Lue880daNiK3BOhlQlZPykUXt1NftMNS/z+e70W+Vpv1ZxCx5BipqZkdoceM3uin0vUQmqmHqjxi5qKUuov90dXLaMxypCA0TDsIDnX8RPvPtqKff1p2TMW2a0XYe7CPYhRggaQMpmo0TcFutgrM1Vywyr2TPxYR+H/tpuuWRET7tUIQykBYoO1WKfL2dX6cxarjAJfnYnod3sMzppHouyp8Pt7gHVG7AgMBAAECggEAEFshSy6IrADKgWSUyH/3jMNZfwnchW6Ar/9O847CAPQJ2yhQIpa/Qpnhs58Y5S2myqcHrUBgFPcWp3BbyGn43naAh8XahWHEcVjWl/N6BV9vM1UKYN0oGikDR3dljCBDbCIoPBBO3WcFOaXoIpaqPmbwCG1aSdwQyPUA0UzG08eDbuHK6L5jvbe3xv5kLpWTVddrocW+SakbZRAX1Ykp7IujOce235nM7GOfoq4b8jmK5CLg6VIZGQV20wnn9YxuFOndRSjneFberzfzBMhVLpPsQ16M2xDLpZaDTggZnq2L6nZygds8Hda++ga3WbD3TcgjJNYuENu1S88IowYhSQKBgQDFqRA+38mo6KsxVDCNWcuEk2hSq8NEUzRHJpS7/QjZmEIYpFzDXgSGwhZJ0WNsQtaxJeBbc7B/OOqh8TL1reLl5AdTimS1OLHWVf/MUsLVS7Y82hx/hpYWxZnRSq41oI3P8FO/53FiQMYo2wbwqF6uQjB1y8h58aqL3OYpTH/5xQKBgQC0mobALJ+bU4nCPzkVDZuD6RyNWPwS1aE3+925wDSN2rJ0iLIb4N5czWZmHb66VlAtfGbp2q+amsCV4r6UR19A/y8k9SFB0mdtxix6mjEfaGhVJm4B1mkvsn0OHMAanKkohUvCjROQc3sziyp2gqSEQ98G7//VMPx/3dhgyQpVfwKBgQCycsqu6N0n+D6t/0MCKiJaI7bYhCd7JN8aqVM4UN5PjG2Hz8PLwbK2cr0qkbaAA+vN7NMb3Vtn0FvMLnUCZqVlRTP0EQqQrYmoZuXUcpdhd8QkNgnqe/g+wND4qcKTucquA1uo8mtj9/Su5+bhGDC6hBk6D+uDZFHDiX/loyIavQKBgQCXF6AcLjjpDZ52b8Yloti0JtXIOuXILAlQeNoqiG5vLsOVUrcPM7VUFlLQo5no8kTpiOXgRyAaS9VKkAO4sW0zR0n9tUY5dvkokV6sw0rNZ9/BPQFTcDlXug99OvhMSzwJtlqHTNdNRg+QM6E2vF0+ejmf6DEz/mN/5e0cK5UFqQKBgCR2hVfbRtDz9Cm/P8chPqaWFkH5ulUxBpc704Igc6bVH5DrEoWo6akbeJixV2obAZO3sFyeJqBUqaCvqG17Xei6jn3Hc3WMz9nLrAJEI9BTCfwvuxCOyY0IxqAAYT28xYv42I4+ADT/PpCq2Dj5u43X0dapAjZBZDfVVis7q1Bw-----END PRIVATE KEY-----';
+
+export async function getFulltext(url: string) {
+ if (!config.caixin.cookie) {
+ return;
+ }
+ if (!/(\d+)\.html/.test(url)) {
+ return;
+ }
+ const articleID = url.match(/(\d+)\.html/)[1];
+
+ const nonce = crypto.randomUUID().replaceAll('-', '').toUpperCase();
+
+ const userID = config.caixin.cookie
+ .split(';')
+ .find((e) => e.includes('SA_USER_UID'))
+ ?.split('=')[1]; //
+
+ const rawString = `id=${articleID}&uid=${userID}&${nonce}=nonce`;
+
+ const sig = new KJUR.crypto.Signature({ alg: 'SHA256withRSA' });
+ sig.init(rsaPrivateKey);
+ sig.updateString(rawString);
+ const sigValueHex = hextob64(sig.sign());
+
+ const isWeekly = url.includes('weekly');
+ const res = await ofetch(`https://gateway.caixin.com/api/newauth/checkAuthByIdJsonp`, {
+ params: {
+ type: 1,
+ page: isWeekly ? 0 : 1,
+ rand: Math.random(),
+ id: articleID,
+ },
+ headers: {
+ 'X-Sign': encodeURIComponent(sigValueHex),
+ 'X-Nonce': encodeURIComponent(nonce),
+ Cookie: config.caixin.cookie,
+ },
+ });
+
+ const { content = '', pictureList } = JSON.parse(res.data.match(/resetContentInfo\((.*)\)/)[1]);
+ return content + (pictureList ? pictureList.map((e) => ` {{ content }} {{ i.embed.description }} ${source} ';
+ if (data.contents && Array.isArray(data.contents)) {
+ html += data.contents.map((data) => extractArticleContent(data)).join('');
+ }
+ html += ' Company: {{ company_name }} Location: {{ location }} Compensation: ${{ yearly_min_compensation_formatted }} - ${{ yearly_max_compensation_formatted }} per year Workplace Type: {{ workplace_type }} Requirements: {{ requirements_summary }} 作者: ${$artist} 价格: ${$price.text().trim()}
{{ item.authors }}
diff --git a/lib/routes/iehou/index.ts b/lib/routes/iehou/index.ts
index cee71b4b71c60e..675096cfb99022 100644
--- a/lib/routes/iehou/index.ts
+++ b/lib/routes/iehou/index.ts
@@ -86,13 +86,13 @@ export const route: Route = {
handler,
example: '/iehou',
parameters: { category: '分类,默认为空,即最新线报,可在对应分类页 URL 中找到' },
- description: `:::tip
+ description: `::: tip
若订阅 [24小时热门线报](https://iehou.com/page-dayhot.htm),网址为 \`https://iehou.com/page-dayhot.htm\`。截取 \`https://iehou.com/page-\` 到末尾 \`.htm\` 的部分 \`dayhot\` 作为参数填入,此时路由为 [\`/iehou/dayhot\`](https://rsshub.app/iehou/dayhot)。
- :::
+:::
- | [最新线报](https://iehou.com/) | [24 小时热门](https://iehou.com/page-dayhot.htm) | [一周热门](https://iehou.com/page-weekhot.htm) |
- | ------------------------------ | ------------------------------------------------ | ---------------------------------------------- |
- | [](https://rsshub.app/iehou) | [dayhot](https://rsshub.app/iehou/dayhot) | [weekhot](https://rsshub.app/iehou/weekhot) |
+| [最新线报](https://iehou.com/) | [24 小时热门](https://iehou.com/page-dayhot.htm) | [一周热门](https://iehou.com/page-weekhot.htm) |
+| ------------------------------ | ------------------------------------------------ | ---------------------------------------------- |
+| [](https://rsshub.app/iehou) | [dayhot](https://rsshub.app/iehou/dayhot) | [weekhot](https://rsshub.app/iehou/weekhot) |
`,
categories: ['new-media'],
diff --git a/lib/routes/iehou/namespace.ts b/lib/routes/iehou/namespace.ts
index 75cb601d83e56b..99cf90351de997 100644
--- a/lib/routes/iehou/namespace.ts
+++ b/lib/routes/iehou/namespace.ts
@@ -5,4 +5,5 @@ export const namespace: Namespace = {
url: 'iehou.com',
categories: ['new-media'],
description: '',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/ielts/index.ts b/lib/routes/ielts/index.ts
index 2d600b8c81e64a..cf025fb9b3aa7f 100644
--- a/lib/routes/ielts/index.ts
+++ b/lib/routes/ielts/index.ts
@@ -38,7 +38,7 @@ async function handler() {
await page.waitForSelector('div.container');
const html = await page.evaluate(() => document.documentElement.innerHTML);
- browser.close();
+ await browser.close();
return html;
},
config.cache.routeExpire,
diff --git a/lib/routes/ielts/namespace.ts b/lib/routes/ielts/namespace.ts
index 0036a906915c88..e310c40d642192 100644
--- a/lib/routes/ielts/namespace.ts
+++ b/lib/routes/ielts/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'IELTS 雅思',
url: 'ielts.neea.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/ifanr/category.ts b/lib/routes/ifanr/category.ts
new file mode 100644
index 00000000000000..38ffd92126035d
--- /dev/null
+++ b/lib/routes/ifanr/category.ts
@@ -0,0 +1,76 @@
+import { Route } from '@/types';
+import got from '@/utils/got';
+import { parseDate } from '@/utils/parse-date';
+
+const PATH_LIST = {
+ 早报: 'ifanrnews',
+ 评测: 'review',
+ 糖纸众测: 'tangzhi-evaluation',
+ 产品: 'product',
+};
+
+export const route: Route = {
+ path: '/category/:name',
+ categories: ['new-media', 'popular'],
+ example: '/ifanr/category/早报',
+ parameters: {
+ name: {
+ description: '分类名称',
+ options: Object.keys(PATH_LIST).map((name) => ({ value: name, label: name })),
+ },
+ },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['www.ifanr.com/category/:name'],
+ },
+ ],
+ name: '分类',
+ maintainers: ['donghongfei'],
+ handler,
+ description: `支持分类:早报、评测、糖纸众测、产品`,
+};
+
+async function handler(ctx) {
+ const name = ctx.req.param('name');
+ const nameEncode = encodeURIComponent(decodeURIComponent(name));
+ const apiUrl = `https://sso.ifanr.com/api/v5/wp/article/?post_category=${nameEncode}&limit=20&offset=0`;
+ const resp = await got({
+ method: 'get',
+ url: apiUrl,
+ });
+ const items = await Promise.all(
+ resp.data.objects.map((item) => {
+ let description = '';
+
+ const banner = item.post_cover_image;
+
+ if (banner) {
+ description = ` (\d{1,2})<\/p>\s* (\d{1,2})\s*\.\s*([A-Za-z]{3})<\/p>/;
+ const match = htmlContent.match(dateRegex);
+
+ if (match) {
+ const day = Number.parseInt(match[1]) + 1;
+ const year = match[2];
+ const monthAbbreviation = match[3];
+
+ const monthMapping: { [key: string]: string } = {
+ Jan: '01',
+ Feb: '02',
+ Mar: '03',
+ Apr: '04',
+ May: '05',
+ Jun: '06',
+ Jul: '07',
+ Aug: '08',
+ Sep: '09',
+ Oct: '10',
+ Nov: '11',
+ Dec: '12',
+ };
+
+ const month = monthMapping[monthAbbreviation] || '';
+
+ return parseDate(`20${year}-${month}-${day}`);
+ }
+
+ return undefined;
+}
diff --git a/lib/routes/jimmyspa/templates/description.art b/lib/routes/jimmyspa/templates/description.art
new file mode 100644
index 00000000000000..dfab19230c1108
--- /dev/null
+++ b/lib/routes/jimmyspa/templates/description.art
@@ -0,0 +1,17 @@
+{{ if images }}
+ {{ each images image }}
+ {{ if image?.src }}
+ `,
- link: item.copyrightlink,
- pubDate: timezone(parseDate(item.fullstartdate), 0),
- })),
+ link: apiUrl,
+ description: 'Bing每日壁纸',
+ item: items,
};
}
diff --git a/lib/routes/bing/namespace.ts b/lib/routes/bing/namespace.ts
index 173fa4a65cca81..abaf432718e02f 100644
--- a/lib/routes/bing/namespace.ts
+++ b/lib/routes/bing/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Bing',
url: 'cn.bing.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/biodiscover/index.ts b/lib/routes/biodiscover/index.ts
index afd6e69cf50338..0b71e5ef4de1b6 100644
--- a/lib/routes/biodiscover/index.ts
+++ b/lib/routes/biodiscover/index.ts
@@ -24,11 +24,11 @@ async function handler(ctx) {
const $ = load(response.data);
const items = $('.new_list .newList_box')
- .map((_, item) => ({
+ .toArray()
+ .map((item) => ({
pubDate: parseDate($(item).find('.news_flow_tag .times').text().trim()),
link: 'http://www.biodiscover.com' + $(item).find('h2 a').attr('href'),
- }))
- .toArray();
+ }));
return {
title: '生物探索 - ' + $('.header li.sel a').text(),
diff --git a/lib/routes/biodiscover/namespace.ts b/lib/routes/biodiscover/namespace.ts
index c40450888e5840..e81481e842ebeb 100644
--- a/lib/routes/biodiscover/namespace.ts
+++ b/lib/routes/biodiscover/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'biodiscover.com 生物探索',
url: 'www.biodiscover.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/bioone/namespace.ts b/lib/routes/bioone/namespace.ts
index 2b4d0a772a3f47..f2a193208ed7b8 100644
--- a/lib/routes/bioone/namespace.ts
+++ b/lib/routes/bioone/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'BioOne',
url: 'bioone.org',
+ lang: 'en',
};
diff --git a/lib/routes/biquge/namespace.ts b/lib/routes/biquge/namespace.ts
index 215a860e0d3906..25da181955d1f8 100644
--- a/lib/routes/biquge/namespace.ts
+++ b/lib/routes/biquge/namespace.ts
@@ -3,7 +3,7 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '笔趣阁',
url: 'xbiquwx.la',
- description: `:::tip
+ description: `::: tip
此处的 **笔趣阁** 指网络上使用和 **笔趣阁** 样式相似模板的小说阅读网站,包括但不限于下方列举的网址。
:::
@@ -24,4 +24,5 @@ export const namespace: Namespace = {
| [https://www.ibiquge.info](https://www.ibiquge.info) | 爱笔楼 |
| [https://www.ishuquge.com](https://www.ishuquge.com) | 书趣阁 |
| [https://www.mayiwxw.com](https://www.mayiwxw.com) | 蚂蚁文学 |`,
+ lang: 'zh-CN',
};
diff --git a/lib/routes/bit/namespace.ts b/lib/routes/bit/namespace.ts
index 3879be0523e216..0566caa96cb222 100644
--- a/lib/routes/bit/namespace.ts
+++ b/lib/routes/bit/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '北京理工大学',
url: 'cs.bit.edu.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/bitbucket/namespace.ts b/lib/routes/bitbucket/namespace.ts
index f414ea13c0ca27..a5607f530fa05a 100644
--- a/lib/routes/bitbucket/namespace.ts
+++ b/lib/routes/bitbucket/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Bitbucket',
url: 'bitbucket.com',
+ lang: 'en',
};
diff --git a/lib/routes/bitget/announcement.ts b/lib/routes/bitget/announcement.ts
new file mode 100644
index 00000000000000..499816a48e6da9
--- /dev/null
+++ b/lib/routes/bitget/announcement.ts
@@ -0,0 +1,201 @@
+import { DataItem, Route, ViewType } from '@/types';
+import ofetch from '@/utils/ofetch';
+import { load } from 'cheerio';
+import cache from '@/utils/cache';
+import { BitgetResponse } from './type';
+import { parseDate } from '@/utils/parse-date';
+import { config } from '@/config';
+
+const handler: Route['handler'] = async (ctx) => {
+ const baseUrl = 'https://www.bitget.com';
+ const announcementApiUrl = `${baseUrl}/v1/msg/push/stationLetterNew`;
+ const { type, lang = 'zh-CN' } = ctx.req.param<'/bitget/announcement/:type/:lang?'>();
+ const languageCode = lang.replace('-', '_');
+ const headers = {
+ Referer: baseUrl,
+ accept: 'application/json, text/plain, */*',
+ 'content-type': 'application/json;charset=UTF-8',
+ language: languageCode,
+ locale: languageCode,
+ };
+ const pageSize = ctx.req.query('limit') ?? '10';
+
+ // stationLetterType: 0 表示全部通知,02 表示新币上线,01 表示最新活动,06 表示最新公告
+ const reqBody: {
+ pageSize: string;
+ openUnread: number;
+ stationLetterType: string;
+ isPre: boolean;
+ lastEndId: null;
+ languageType: number;
+ excludeStationLetterType?: string;
+ } = {
+ pageSize,
+ openUnread: 0,
+ stationLetterType: '0',
+ isPre: false,
+ lastEndId: null,
+ languageType: 1,
+ };
+
+ // 根据 type 判断 reqBody 的 stationLetterType 的值
+ switch (type) {
+ case 'new-listing':
+ reqBody.stationLetterType = '02';
+ break;
+
+ case 'latest-activities':
+ reqBody.stationLetterType = '01';
+ break;
+
+ case 'new-announcement':
+ reqBody.stationLetterType = '06';
+ break;
+
+ case 'all':
+ reqBody.stationLetterType = '0';
+ reqBody.excludeStationLetterType = '00';
+ break;
+
+ default:
+ throw new Error('Invalid type');
+ }
+
+ const response = (await cache.tryGet(
+ `bitget:announcement:${type}:${pageSize}:${lang}`,
+ async () => {
+ const result = await ofetch
'),
+ embed: post.embed,
+ // embed.$type "app.bsky.embed.record#view" and "app.bsky.embed.recordWithMedia#view" are not handled
+ }),
+ 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]}`,
+ upvotes: post.likeCount,
+ comments: post.replyCount,
+ }));
+
+ ctx.set('json', {
+ DID,
+ profile,
+ feeds,
+ });
+
+ return {
+ title: `${profile.view.displayName} — Bluesky`,
+ description: profile.view.description?.replaceAll('\n', ' '),
+ link: `https://bsky.app/profile/${handle}/feed/${space}`,
+ image: profile.view.avatar,
+ icon: profile.view.avatar,
+ logo: profile.view.avatar,
+ item: items,
+ allowEmpty: true,
+ };
+}
diff --git a/lib/routes/bsky/namespace.ts b/lib/routes/bsky/namespace.ts
index a9241da5158bae..634fd3ab4bae08 100644
--- a/lib/routes/bsky/namespace.ts
+++ b/lib/routes/bsky/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Bluesky (bsky)',
url: 'bsky.app',
+ lang: 'en',
};
diff --git a/lib/routes/bsky/posts.ts b/lib/routes/bsky/posts.ts
index a5a9540c2fbd36..8beb29b1c460f3 100644
--- a/lib/routes/bsky/posts.ts
+++ b/lib/routes/bsky/posts.ts
@@ -1,4 +1,4 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import { getCurrentPath } from '@/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);
@@ -7,12 +7,17 @@ import { parseDate } from '@/utils/parse-date';
import { resolveHandle, getProfile, getAuthorFeed } from './utils';
import { art } from '@/utils/render';
import path from 'node:path';
+import querystring from 'querystring';
export const route: Route = {
- path: '/profile/:handle',
- categories: ['social-media'],
+ path: '/profile/:handle/:routeParams?',
+ categories: ['social-media', 'popular'],
+ view: ViewType.SocialMedia,
example: '/bsky/profile/bsky.app',
- parameters: { handle: 'User handle, can be found in URL' },
+ parameters: {
+ handle: 'User handle, can be found in URL',
+ routeParams: 'Filter parameter, Use filter to customize content types',
+ },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -29,21 +34,35 @@ export const route: Route = {
name: 'Post',
maintainers: ['TonyRL'],
handler,
+ description: `
+| Filter Value | Description |
+|--------------|-------------|
+| posts_with_replies | Includes Posts, Replies, and Reposts |
+| posts_no_replies | Includes Posts and Reposts, without Replies |
+| posts_with_media | Shows only Posts containing media |
+| posts_and_author_threads | Shows Posts and Threads, without Replies and Reposts |
+
+Default value for filter is \`posts_and_author_threads\` if not specified.
+
+Example:
+- \`/bsky/profile/bsky.app/filter=posts_with_replies\``,
};
async function handler(ctx) {
const handle = ctx.req.param('handle');
+ const routeParams = querystring.parse(ctx.req.param('routeParams'));
+ const filter = routeParams.filter || 'posts_and_author_threads';
+
const DID = await resolveHandle(handle, cache.tryGet);
const profile = await getProfile(DID, cache.tryGet);
- const authorFeed = await getAuthorFeed(DID, cache.tryGet);
+ const authorFeed = await getAuthorFeed(DID, filter, cache.tryGet);
const items = authorFeed.feed.map(({ post }) => ({
title: post.record.text.split('\n')[0],
description: art(path.join(__dirname, 'templates/post.art'), {
text: post.record.text.replaceAll('\n', '
'),
embed: post.embed,
- // embed.$type "app.bsky.embed.record#view" and "app.bsky.embed.recordWithMedia#view"
- // are not handled
+ // embed.$type "app.bsky.embed.record#view" and "app.bsky.embed.recordWithMedia#view" are not handled
}),
author: post.author.displayName,
pubDate: parseDate(post.record.createdAt),
@@ -62,9 +81,10 @@ async function handler(ctx) {
title: `${profile.displayName} (@${profile.handle}) — Bluesky`,
description: profile.description?.replaceAll('\n', ' '),
link: `https://bsky.app/profile/${profile.handle}`,
- image: profile.banner,
+ image: profile.avatar,
icon: profile.avatar,
logo: profile.avatar,
item: items,
+ allowEmpty: true,
};
}
diff --git a/lib/routes/bsky/templates/post.art b/lib/routes/bsky/templates/post.art
index 06b42960a4de92..80d41fea1844ca 100644
--- a/lib/routes/bsky/templates/post.art
+++ b/lib/routes/bsky/templates/post.art
@@ -3,11 +3,20 @@
{{ /if }}
{{ if embed }}
- {{ if embed.$type == 'app.bsky.embed.images#view'}}
+ {{ if embed.$type === 'app.bsky.embed.images#view' }}
{{ each embed.images i }}
{{ /each }}
- {{ else if embed.$type == 'app.bsky.embed.external#view' }}
+ {{ else if embed.$type === 'app.bsky.embed.video#view' }}
+
+ {{ else if embed.$type === 'app.bsky.embed.external#view' }}
{{ embed.external.title }}
{{ embed.external.description }}
diff --git a/lib/routes/bsky/utils.ts b/lib/routes/bsky/utils.ts
index 3ffe67f638bf3f..654a2eda7c3df8 100644
--- a/lib/routes/bsky/utils.ts
+++ b/lib/routes/bsky/utils.ts
@@ -28,14 +28,14 @@ const getProfile = (did, tryGet) =>
});
// https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getAuthorFeed.json
-const getAuthorFeed = (did, tryGet) =>
+const getAuthorFeed = (did, filter, tryGet) =>
tryGet(
- `bsky:authorFeed:${did}`,
+ `bsky:authorFeed:${did}:${filter}`,
async () => {
const { data } = await got('https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed', {
searchParams: {
actor: did,
- filter: 'posts_and_author_threads',
+ filter,
limit: 30,
},
});
@@ -45,4 +45,37 @@ const getAuthorFeed = (did, tryGet) =>
false
);
-export { resolveHandle, getProfile, getAuthorFeed };
+// https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getFeed.json
+const getFeed = (uri, tryGet) =>
+ tryGet(
+ `bsky:feed:${uri}`,
+ async () => {
+ const { data } = await got('https://public.api.bsky.app/xrpc/app.bsky.feed.getFeed', {
+ searchParams: {
+ feed: uri,
+ limit: 30,
+ },
+ });
+ return data;
+ },
+ config.cache.routeExpire,
+ false
+ );
+
+// https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getFeedGenerator.json
+const getFeedGenerator = (uri, tryGet) =>
+ tryGet(
+ `bsky:feedGenerator:${uri}`,
+ async () => {
+ const { data } = await got('https://public.api.bsky.app/xrpc/app.bsky.feed.getFeedGenerator', {
+ searchParams: {
+ feed: uri,
+ },
+ });
+ return data;
+ },
+ config.cache.routeExpire,
+ false
+ );
+
+export { resolveHandle, getProfile, getFeed, getAuthorFeed, getFeedGenerator };
diff --git a/lib/routes/bt0/mv.ts b/lib/routes/bt0/mv.ts
new file mode 100644
index 00000000000000..36ef8d33e95c3c
--- /dev/null
+++ b/lib/routes/bt0/mv.ts
@@ -0,0 +1,65 @@
+import { Route } from '@/types';
+import InvalidParameterError from '@/errors/types/invalid-parameter';
+import { doGot, genSize } from './util';
+
+export const route: Route = {
+ path: '/mv/:number/:domain?',
+ categories: ['multimedia'],
+ example: '/bt0/mv/35575567/2',
+ parameters: { number: '影视详情id, 网页路径为`/mv/{id}.html`其中的id部分, 一般为8位纯数字', domain: '数字1-9, 比如1表示请求域名为 1bt0.com, 默认为 2' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: true,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['2bt0.com/mv/'],
+ },
+ ],
+ name: '影视资源下载列表',
+ maintainers: ['miemieYaho'],
+ handler,
+};
+
+async function handler(ctx) {
+ const domain = ctx.req.param('domain') ?? '2';
+ const number = ctx.req.param('number');
+ if (!/^[1-9]$/.test(domain)) {
+ throw new InvalidParameterError('Invalid domain');
+ }
+ const regex = /^\d{6,}$/;
+ if (!regex.test(number)) {
+ throw new InvalidParameterError('Invalid number');
+ }
+
+ const host = `https://www.${domain}bt0.com`;
+ const _link = `${host}/prod/core/system/getVideoDetail/${number}`;
+
+ const data = (await doGot(0, host, _link)).data;
+ const items = Object.values(data.ecca).flatMap((item) =>
+ item.map((i) => ({
+ title: i.zname,
+ guid: i.zname,
+ description: `${i.zname}[${i.zsize}]`,
+ link: `${host}/tr/${i.id}.html`,
+ pubDate: i.ezt,
+ enclosure_type: 'application/x-bittorrent',
+ enclosure_url: i.zlink,
+ enclosure_length: genSize(i.zsize),
+ category: strsJoin(i.zqxd, i.text_html, i.audio_html, i.new === 1 ? '新' : ''),
+ }))
+ );
+ return {
+ title: data.title,
+ link: `${host}/mv/${number}.html`,
+ item: items,
+ };
+}
+
+function strsJoin(...strings) {
+ return strings.filter((str) => str !== '').join(',');
+}
diff --git a/lib/routes/bt0/namespace.ts b/lib/routes/bt0/namespace.ts
new file mode 100644
index 00000000000000..9aec8c046775e0
--- /dev/null
+++ b/lib/routes/bt0/namespace.ts
@@ -0,0 +1,10 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '不太灵影视',
+ url: '2bt0.com',
+ description: `::: tip
+ (1-9)bt0.com 都指向同一个
+:::`,
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/bt0/tlist.ts b/lib/routes/bt0/tlist.ts
new file mode 100644
index 00000000000000..481eab960d8b4b
--- /dev/null
+++ b/lib/routes/bt0/tlist.ts
@@ -0,0 +1,67 @@
+import { Route } from '@/types';
+import InvalidParameterError from '@/errors/types/invalid-parameter';
+import { doGot, genSize } from './util';
+import { parseRelativeDate } from '@/utils/parse-date';
+
+const categoryDict = {
+ 1: '电影',
+ 2: '电视剧',
+ 3: '近日热门',
+ 4: '本周热门',
+ 5: '本月热门',
+};
+
+export const route: Route = {
+ path: '/tlist/:sc/:domain?',
+ categories: ['multimedia'],
+ example: '/bt0/tlist/1',
+ parameters: { sc: '分类(1-5), 1:电影, 2:电视剧, 3:近日热门, 4:本周热门, 5:本月热门', domain: '数字1-9, 比如1表示请求域名为 1bt0.com, 默认为 2' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: true,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['2bt0.com/tlist/'],
+ },
+ ],
+ name: '最新资源列表',
+ maintainers: ['miemieYaho'],
+ handler,
+};
+
+async function handler(ctx) {
+ const domain = ctx.req.param('domain') ?? '2';
+ const sc = ctx.req.param('sc');
+ if (!/^[1-9]$/.test(domain)) {
+ throw new InvalidParameterError('Invalid domain');
+ }
+ if (!/^[1-5]$/.test(sc)) {
+ throw new InvalidParameterError('Invalid sc');
+ }
+
+ const host = `https://www.${domain}bt0.com`;
+ const _link = `${host}/prod/core/system/getTList?sc=${sc}`;
+
+ const data = await doGot(0, host, _link);
+ const items = data.data.list.map((item) => ({
+ title: item.zname,
+ guid: item.zname,
+ description: `《${item.title}》 导演: ${item.daoyan}
编剧: ${item.bianji}
演员: ${item.yanyuan}
简介: ${item.conta.trim()}`,
+ link: host + item.aurl,
+ pubDate: item.eztime.endsWith('前') ? parseRelativeDate(item.eztime) : item.eztime,
+ enclosure_type: 'application/x-bittorrent',
+ enclosure_url: item.zlink,
+ enclosure_length: genSize(item.zsize),
+ itunes_item_image: item.epic,
+ }));
+ return {
+ title: `不太灵-最新资源列表-${categoryDict[sc]}`,
+ link: `${host}/tlist/${sc}_1.html`,
+ item: items,
+ };
+}
diff --git a/lib/routes/bt0/util.ts b/lib/routes/bt0/util.ts
new file mode 100644
index 00000000000000..1cb34774af4275
--- /dev/null
+++ b/lib/routes/bt0/util.ts
@@ -0,0 +1,51 @@
+import { CookieJar } from 'tough-cookie';
+import got from '@/utils/got';
+const cookieJar = new CookieJar();
+
+async function doGot(num, host, link) {
+ if (num > 4) {
+ throw new Error('The number of attempts has exceeded 5 times');
+ }
+ const response = await got.get(link, {
+ cookieJar,
+ });
+ const data = response.data;
+ if (typeof data === 'string') {
+ const regex = /document\.cookie\s*=\s*"([^"]*)"/;
+ const match = data.match(regex);
+ if (!match) {
+ throw new Error('api error');
+ }
+ cookieJar.setCookieSync(match[1], host);
+ return doGot(++num, host, link);
+ }
+ return data;
+}
+
+const genSize = (sizeStr) => {
+ // 正则表达式,用于匹配数字和单位 GB 或 MB
+ const regex = /^(\d+(\.\d+)?)\s*(gb|mb)$/i;
+ const match = sizeStr.match(regex);
+
+ if (!match) {
+ return 0;
+ }
+
+ const value = Number.parseFloat(match[1]);
+ const unit = match[3].toUpperCase();
+
+ let bytes;
+ switch (unit) {
+ case 'GB':
+ bytes = Math.floor(value * 1024 * 1024 * 1024);
+ break;
+ case 'MB':
+ bytes = Math.floor(value * 1024 * 1024);
+ break;
+ default:
+ bytes = 0;
+ }
+ return bytes;
+};
+
+export { doGot, genSize };
diff --git a/lib/routes/btzj/index.ts b/lib/routes/btzj/index.ts
index 58687e1a240098..b665838cd533fd 100644
--- a/lib/routes/btzj/index.ts
+++ b/lib/routes/btzj/index.ts
@@ -35,37 +35,37 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
url: 'btbtt20.com/',
- description: `:::tip
+ description: `::: tip
分类页中域名末尾到 \`.htm\` 前的字段即为对应分类,如 [电影](https://www.btbtt20.com/forum-index-fid-951.htm) \`https://www.btbtt20.com/forum-index-fid-951.htm\` 中域名末尾到 \`.htm\` 前的字段为 \`forum-index-fid-951\`,所以路由应为 [\`/btzj/forum-index-fid-951\`](https://rsshub.app/btzj/forum-index-fid-951)
部分分类页,如 [电影](https://www.btbtt20.com/forum-index-fid-951.htm)、[剧集](https://www.btbtt20.com/forum-index-fid-950.htm) 等,提供了更复杂的分类筛选。你可以将选项选中后,获得结果分类页 URL 中分类参数,构成路由。如选中分类 [高清电影 - 年份:2021 - 地区:欧美](https://www.btbtt20.com/forum-index-fid-1183-typeid1-0-typeid2-738-typeid3-10086-typeid4-0.htm) \`https://www.btbtt20.com/forum-index-fid-1183-typeid1-0-typeid2-738-typeid3-10086-typeid4-0.htm\` 中域名末尾到 \`.htm\` 前的字段为 \`forum-index-fid-1183-typeid1-0-typeid2-738-typeid3-10086-typeid4-0\`,所以路由应为 [\`/btzj/forum-index-fid-1183-typeid1-0-typeid2-738-typeid3-10086-typeid4-0\`](https://rsshub.app/btzj/forum-index-fid-1183-typeid1-0-typeid2-738-typeid3-10086-typeid4-0)
- :::
+:::
基础分类如下:
- | 交流 | 电影 | 剧集 | 高清电影 |
- | ------------------- | ------------------- | ------------------- | -------------------- |
- | forum-index-fid-975 | forum-index-fid-951 | forum-index-fid-950 | forum-index-fid-1183 |
+| 交流 | 电影 | 剧集 | 高清电影 |
+| ------------------- | ------------------- | ------------------- | -------------------- |
+| forum-index-fid-975 | forum-index-fid-951 | forum-index-fid-950 | forum-index-fid-1183 |
- | 音乐 | 动漫 | 游戏 | 综艺 |
- | ------------------- | ------------------- | ------------------- | -------------------- |
- | forum-index-fid-953 | forum-index-fid-981 | forum-index-fid-955 | forum-index-fid-1106 |
+| 音乐 | 动漫 | 游戏 | 综艺 |
+| ------------------- | ------------------- | ------------------- | -------------------- |
+| forum-index-fid-953 | forum-index-fid-981 | forum-index-fid-955 | forum-index-fid-1106 |
- | 图书 | 美图 | 站务 | 科技 |
- | -------------------- | ------------------- | ----------------- | ------------------- |
- | forum-index-fid-1151 | forum-index-fid-957 | forum-index-fid-2 | forum-index-fid-952 |
+| 图书 | 美图 | 站务 | 科技 |
+| -------------------- | ------------------- | ----------------- | ------------------- |
+| forum-index-fid-1151 | forum-index-fid-957 | forum-index-fid-2 | forum-index-fid-952 |
- | 求助 | 音轨字幕 |
- | -------------------- | -------------------- |
- | forum-index-fid-1187 | forum-index-fid-1191 |
+| 求助 | 音轨字幕 |
+| -------------------- | -------------------- |
+| forum-index-fid-1187 | forum-index-fid-1191 |
- :::tip
+::: tip
BT 之家的域名会变更,本路由以 \`https://www.btbtt20.com\` 为默认域名,若该域名无法访问,可以通过在路由后方加上 \`?domain=<域名>\` 指定路由访问的域名。如指定域名为 \`https://www.btbtt15.com\`,则在 \`/btzj\` 后加上 \`?domain=btbtt15.com\` 即可,此时路由为 [\`/btzj?domain=btbtt15.com\`](https://rsshub.app/btzj?domain=btbtt15.com)
如果加入了分类参数,直接在分类参数后加入 \`?domain=<域名>\` 即可。如指定分类 [剧集](https://www.btbtt20.com/forum-index-fid-950.htm) \`https://www.btbtt20.com/forum-index-fid-950.htm\` 并指定域名为 \`https://www.btbtt15.com\`,即在 \`/btzj/forum-index-fid-950\` 后加上 \`?domain=btbtt15.com\`,此时路由为 [\`/btzj/forum-index-fid-950?domain=btbtt15.com\`](https://rsshub.app/btzj/forum-index-fid-950?domain=btbtt15.com)
目前,你可以选择的域名有 \`btbtt10-20.com\` 共 10 个,或 \`88btbbt.com\`,该站也提供了专用网址查询工具。详见 [此贴](https://www.btbtt20.com/thread-index-fid-2-tid-4550191.htm)
- :::`,
+:::`,
};
async function handler(ctx) {
diff --git a/lib/routes/btzj/namespace.ts b/lib/routes/btzj/namespace.ts
index f5847215f8b37d..f6fea9869ef79c 100644
--- a/lib/routes/btzj/namespace.ts
+++ b/lib/routes/btzj/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'BT 之家',
url: 'btbtt20.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/buaa/jiaowu.ts b/lib/routes/buaa/jiaowu.ts
new file mode 100644
index 00000000000000..2bb9cb861afece
--- /dev/null
+++ b/lib/routes/buaa/jiaowu.ts
@@ -0,0 +1,124 @@
+import { Data, Route } from '@/types';
+import { Context } from 'hono';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+
+const BASE_URL = 'https://jiaowu.buaa.edu.cn/bhjwc2.0/index/newsList.do';
+
+export const route: Route = {
+ path: '/jiaowu/:cddm?',
+ name: '教务部',
+ url: 'jiaowu.buaa.edu.cn',
+ maintainers: ['OverflowCat'],
+ handler,
+ example: '/buaa/jiaowu/02',
+ parameters: {
+ cddm: '菜单代码,可以是 2 位或者 4 位,默认为 `02`(通知公告)',
+ },
+ description: `::: tip
+
+菜单代码(\`cddm\`)应填写链接中调用的 newsList 接口的参数,可以是 2 位或者 4 位数字。若为 2 位,则为 \`fcd\`(父菜单);若为 4 位,则为 \`cddm\`(菜单代码),其中前 2 位为 \`fcd\`。
+示例:
+
+1. 新闻快讯页面的链接中 \`onclick="javascript:onNewsList('03');return false;"\`,对应的路径参数为 \`03\`,完整路由为 \`/buaa/jiaowu/03\`;
+2. 通知公告 > 公示专区页面的链接中 \`onclick="javascript:onNewsList2('0203','2');return false;"\`,对应的路径参数为 \`0203\`,完整路由为 \`/buaa/jiaowu/0203\`。
+:::`,
+ categories: ['university'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+};
+
+async function handler(ctx: Context): Promise {
+ let cddm = ctx.req.param('cddm');
+ if (!cddm) {
+ cddm = '02';
+ }
+ if (cddm.length !== 2 && cddm.length !== 4) {
+ throw new Error('cddm should be 2 or 4 digits');
+ }
+
+ const { title, list } = await getList(BASE_URL, {
+ id: '',
+ fcdTab: cddm.slice(0, 2),
+ cddmTab: cddm,
+ xsfsTab: '2',
+ tplbid: '',
+ xwid: '',
+ zydm: '',
+ zymc: '',
+ yxdm: '',
+ pyzy: '',
+ szzqdm: '',
+ });
+ const item = await getItems(list);
+
+ return {
+ title,
+ item,
+ link: BASE_URL,
+ author: '北航教务部',
+ language: 'zh-CN',
+ };
+}
+
+function getArticleUrl(onclick?: string) {
+ if (!onclick) {
+ return null;
+ }
+ const xwid = onclick.match(/'(\d+)'/)?.at(1);
+ if (!xwid) {
+ return null;
+ }
+ return `http://jiaowu.buaa.edu.cn/bhjwc2.0/index/newsView.do?xwid=${xwid}`;
+}
+
+async function getList(url: string | URL, form: Record
');
+ item.author = $descrption('#main > div.content > div.search_height > span.search_con').text().split('发布者:').at(-1) || '教务部';
+ return item;
+ })
+ )
+ );
+}
diff --git a/lib/routes/buaa/lib/space/newbook.ts b/lib/routes/buaa/lib/space/newbook.ts
new file mode 100644
index 00000000000000..810b8ef3cf5882
--- /dev/null
+++ b/lib/routes/buaa/lib/space/newbook.ts
@@ -0,0 +1,171 @@
+import { Data, DataItem, Route } from '@/types';
+import { Context } from 'hono';
+import got from '@/utils/got';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+import cache from '@/utils/cache';
+import { art } from '@/utils/render';
+import path from 'node:path';
+import { getCurrentPath } from '@/utils/helpers';
+const __dirname = getCurrentPath(import.meta.url);
+
+interface Book {
+ bibId: string;
+ inBooklist: number;
+ thumb: string;
+ holdingTypes: string[];
+ author: string;
+ callno: string[];
+ docType: string;
+ onSelfDate: string;
+ groupId: string;
+ isbn: string;
+ inDate: number;
+ language: string;
+ bibNo: string;
+ abstract: string;
+ docTypeDesc: string;
+ title: string;
+ itemCount: number;
+ tags: string[];
+ circCount: number;
+ pub_year: string;
+ classno: string;
+ publisher: string;
+ holdings: string;
+}
+
+interface Holding {
+ classMethod: string;
+ callNo: string;
+ inDate: number;
+ shelfMark: string;
+ itemsCount: number;
+ barCode: string;
+ tempLocation: string;
+ circStatus: number;
+ itemId: number;
+ vol: string;
+ library: string;
+ itemStatus: string;
+ itemsAvailable: number;
+ location: string;
+ extenStatus: number;
+ donatorId: null;
+ status: string;
+ locationName: string;
+}
+
+interface Info {
+ _id: string;
+ imageUrl: string | null;
+ authorInfo: string;
+ catalog: string | null;
+ content: string;
+ title: string;
+}
+
+export const route: Route = {
+ path: String.raw`/lib/space/:path{newbook.*}`,
+ name: '图书馆 - 新书速递',
+ url: 'space.lib.buaa.edu.cn/mspace/newBook',
+ maintainers: ['OverflowCat'],
+ example: '/buaa/lib/space/newbook/',
+ handler,
+ description: `可通过参数进行筛选:\`/buaa/lib/space/newbook/key1=value1&key2=value2...\`
+- \`dcpCode\`:学科分类代码
+ - 例:
+ - 工学:\`08\`
+ - 工学 > 计算机 > 计算机科学与技术:\`080901\`
+ - 默认值:\`nolimit\`
+ - 注意事项:不可与 \`clsNo\` 同时使用。
+- \`clsNo\`:中图分类号
+ - 例:
+ - 计算机科学:\`TP3\`
+ - 默认值:无
+ - 注意事项
+ - 不可与 \`dcpCode\` 同时使用。
+ - 此模式下获取不到上架日期。
+- \`libCode\`:图书馆代码
+ - 例:
+ - 本馆:\`00000\`
+ - 默认值:无
+ - 注意事项:只有本馆一个可选值。
+- \`locaCode\`:馆藏地代码
+ - 例:
+ - 五层西-中文新书借阅室(A-Z类):\`02503\`
+ - 默认值:无
+ - 注意事项:必须与 \`libCode\` 同时使用。
+
+示例:
+- \`buaa/lib/space/newbook\` 为所有新书
+- \`buaa/lib/space/newbook/clsNo=U&libCode=00000&locaCode=60001\` 为沙河教2图书馆所有中图分类号为 U(交通运输)的书籍
+`,
+ categories: ['university'],
+
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+};
+
+async function handler(ctx: Context): Promise {
+ const path = ctx.req.param('path');
+ const i = path.indexOf('/');
+ const params = i === -1 ? '' : path.slice(i + 1);
+ const searchParams = new URLSearchParams(params);
+ const dcpCode = searchParams.get('dcpCode'); // Filter by subject (discipline code)
+ const clsNo = searchParams.get('clsNo'); // Filter by class (Chinese Library Classification)
+ if (dcpCode && clsNo) {
+ throw new Error('dcpCode and clsNo cannot be used at the same time');
+ }
+ searchParams.set('pageSize', '100'); // Max page size. Any larger value will be ignored
+ searchParams.set('page', '1');
+ !dcpCode && !clsNo && searchParams.set('dcpCode', 'nolimit'); // No classification filter
+ const url = `https://space.lib.buaa.edu.cn/meta-local/opac/new/100/${clsNo ? 'byclass' : 'bysubject'}?${searchParams.toString()}`;
+ const { data } = await got(url);
+ const list = (data?.data?.dataList || []) as Book[];
+ const item = await Promise.all(list.map(async (item: Book) => await getItem(item)));
+ const res: Data = {
+ title: '北航图书馆 - 新书速递',
+ item,
+ description: '北京航空航天大学图书馆新书速递',
+ language: 'zh-CN',
+ link: 'https://space.lib.buaa.edu.cn/space/newBook',
+ author: '北京航空航天大学图书馆',
+ allowEmpty: true,
+ image: 'https://lib.buaa.edu.cn/apple-touch-icon.png',
+ };
+ return res;
+}
+
+async function getItem(item: Book): Promise书籍信息
+简介
+
+
+{{if info.authorInfo}}
+
+ ISBN {{item.isbn}}
+ 语言 {{item.language}}
+类型 {{item.docTypeDesc}} 作者简介
+馆藏信息
+{{if item.onSelfDate}}
+上架时间:
+
+馆藏地点
+
+ {{each holdings holding}}
+
+{{if info.catalog}}
+
+ 所属馆藏地 {{holding.location}}
+ 索书号 {{holding.callNo}}
+ 条码号 {{holding.barCode}}
+ 编号 {{holding.itemId}}
+
+ {{/each}}
+书刊状态
+ {{holding.status}}
+ 目录
+
`).join('') : '');
+}
diff --git a/lib/routes/caixin/utils.ts b/lib/routes/caixin/utils.ts
index fca14c8761199c..0bd0334e3bb421 100644
--- a/lib/routes/caixin/utils.ts
+++ b/lib/routes/caixin/utils.ts
@@ -6,44 +6,44 @@ import { load } from 'cheerio';
import { art } from '@/utils/render';
import path from 'node:path';
-const parseArticle = (item, tryGet) =>
- /\.blog\.caixin\.com$/.test(new URL(item.link).hostname)
- ? parseBlogArticle(item, tryGet)
- : tryGet(item.link, async () => {
- const { data: response } = await got(item.link);
-
- const $ = load(response);
-
- item.description = art(path.join(__dirname, 'templates/article.art'), {
- 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 = (item, tryGet) =>
- tryGet(item.link, async () => {
- const response = await got(item.link);
- const $ = load(response.data);
- const article = $('#the_content').removeAttr('style');
- article.find('img').removeAttr('style');
- article
- .find('p')
- // Non-breaking space U+00A0, ` ` in html
- // element.children[0].data === $(element, article).text()
- .filter((_, element) => element.children[0].data === String.fromCharCode(160))
- .remove();
-
- item.description = article.html();
+const parseArticle = async (item) => {
+ if (/\.blog\.caixin\.com$/.test(new URL(item.link).hostname)) {
+ return parseBlogArticle(item);
+ } else {
+ const { data: response } = await got(item.link);
+
+ const $ = load(response);
+
+ item.description = art(path.join(__dirname, 'templates/article.art'), {
+ 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) => {
+ const response = await got(item.link);
+ const $ = load(response.data);
+ const article = $('#the_content').removeAttr('style');
+ article.find('img').removeAttr('style');
+ article
+ .find('p')
+ // Non-breaking space U+00A0, ` ` in html
+ // element.children[0].data === $(element, article).text()
+ .filter((_, element) => element.children[0].data === String.fromCodePoint(160))
+ .remove();
+
+ item.description = article.html();
+
+ return item;
+};
export { parseArticle, parseBlogArticle };
diff --git a/lib/routes/caixinglobal/namespace.ts b/lib/routes/caixinglobal/namespace.ts
index bf1e6f1aed92b6..8d4880dd8533cc 100644
--- a/lib/routes/caixinglobal/namespace.ts
+++ b/lib/routes/caixinglobal/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Caixin Global',
url: 'caixinglobal.com',
+ lang: 'en',
};
diff --git a/lib/routes/camchina/index.ts b/lib/routes/camchina/index.ts
index ebe8624f9e36bf..0c7976c9b39b42 100644
--- a/lib/routes/camchina/index.ts
+++ b/lib/routes/camchina/index.ts
@@ -25,8 +25,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 新闻 | 通告栏 |
- | ---- | ------ |
- | 1 | 2 |`,
+| ---- | ------ |
+| 1 | 2 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/camchina/namespace.ts b/lib/routes/camchina/namespace.ts
index d2b1d471d88720..468a798bd240ff 100644
--- a/lib/routes/camchina/namespace.ts
+++ b/lib/routes/camchina/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国管理现代化研究会',
url: 'cste.org.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cankaoxiaoxi/index.ts b/lib/routes/cankaoxiaoxi/index.ts
index 4c94ee68346dbd..9ed772ea6e4848 100644
--- a/lib/routes/cankaoxiaoxi/index.ts
+++ b/lib/routes/cankaoxiaoxi/index.ts
@@ -26,26 +26,26 @@ export const route: Route = {
maintainers: ['yuxinliu-alex', 'nczitzk'],
handler,
description: `| 栏目 | id |
- | -------------- | -------- |
- | 第一关注 | diyi |
- | 中国 | zhongguo |
- | 国际 | gj |
- | 观点 | guandian |
- | 锐参考 | ruick |
- | 体育健康 | tiyujk |
- | 科技应用 | kejiyy |
- | 文化旅游 | wenhualy |
- | 参考漫谈 | cankaomt |
- | 研究动态 | yjdt |
- | 海外智库 | hwzk |
- | 业界信息・观点 | yjxx |
- | 海外看中国城市 | hwkzgcs |
- | 译名趣谈 | ymymqt |
- | 译名发布 | ymymfb |
- | 双语汇 | ymsyh |
- | 参考视频 | video |
- | 军事 | junshi |
- | 参考人物 | cankaorw |`,
+| -------------- | -------- |
+| 第一关注 | diyi |
+| 中国 | zhongguo |
+| 国际 | gj |
+| 观点 | guandian |
+| 锐参考 | ruick |
+| 体育健康 | tiyujk |
+| 科技应用 | kejiyy |
+| 文化旅游 | wenhualy |
+| 参考漫谈 | cankaomt |
+| 研究动态 | yjdt |
+| 海外智库 | hwzk |
+| 业界信息・观点 | yjxx |
+| 海外看中国城市 | hwkzgcs |
+| 译名趣谈 | ymymqt |
+| 译名发布 | ymymfb |
+| 双语汇 | ymsyh |
+| 参考视频 | video |
+| 军事 | junshi |
+| 参考人物 | cankaorw |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cankaoxiaoxi/namespace.ts b/lib/routes/cankaoxiaoxi/namespace.ts
index 13e98f6d4ecffc..8520e948b2e095 100644
--- a/lib/routes/cankaoxiaoxi/namespace.ts
+++ b/lib/routes/cankaoxiaoxi/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '参考消息',
url: 'cankaoxiaoxi.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cara/constant.ts b/lib/routes/cara/constant.ts
new file mode 100644
index 00000000000000..4f1b7bec4fcdd1
--- /dev/null
+++ b/lib/routes/cara/constant.ts
@@ -0,0 +1,5 @@
+export const HOST = 'https://cara.app';
+
+export const API_HOST = `${HOST}/api`;
+
+export const CDN_HOST = 'https://cdn.cara.app';
diff --git a/lib/routes/cara/likes.ts b/lib/routes/cara/likes.ts
new file mode 100644
index 00000000000000..bee254f5a6730d
--- /dev/null
+++ b/lib/routes/cara/likes.ts
@@ -0,0 +1,56 @@
+import type { Data, DataItem, Route } from '@/types';
+import type { PostsResponse } from './types';
+import { customFetch, parseUserData } from './utils';
+import { API_HOST, CDN_HOST, HOST } from './constant';
+import { getCurrentPath } from '@/utils/helpers';
+import { art } from '@/utils/render';
+import { parseDate } from '@/utils/parse-date';
+import path from 'node:path';
+
+const __dirname = getCurrentPath(import.meta.url);
+
+export const route: Route = {
+ path: ['/likes/:user'],
+ categories: ['social-media', 'popular'],
+ example: '/cara/likes/fengz',
+ parameters: { user: 'username' },
+ name: 'Likes',
+ maintainers: ['KarasuShin'],
+ handler,
+ radar: [
+ {
+ source: ['cara.app/:user', 'cara.app/:user/*'],
+ target: '/likes/:user',
+ },
+ ],
+};
+
+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 userInfo = await parseUserData(user);
+
+ const api = `${API_HOST}/posts/getAllLikesByUser?slug=${userInfo.slug}&take=${limit}`;
+
+ const timelineResponse = await customFetch
+{{ /each }}
diff --git a/lib/routes/cara/timeline.ts b/lib/routes/cara/timeline.ts
new file mode 100644
index 00000000000000..ebdd485801bbf5
--- /dev/null
+++ b/lib/routes/cara/timeline.ts
@@ -0,0 +1,56 @@
+import type { Data, DataItem, Route } from '@/types';
+import type { PostsResponse } from './types';
+import { customFetch, parseUserData } from './utils';
+import { API_HOST, CDN_HOST, HOST } from './constant';
+import { getCurrentPath } from '@/utils/helpers';
+import { art } from '@/utils/render';
+import { parseDate } from '@/utils/parse-date';
+import path from 'node:path';
+
+const __dirname = getCurrentPath(import.meta.url);
+
+export const route: Route = {
+ path: ['/timeline/:user'],
+ categories: ['social-media', 'popular'],
+ example: '/cara/timeline/fengz',
+ parameters: { user: 'username' },
+ name: 'Timeline',
+ maintainers: ['KarasuShin'],
+ handler,
+ radar: [
+ {
+ source: ['cara.app/:user', 'cara.app/:user/*'],
+ target: '/timeline/:user',
+ },
+ ],
+};
+
+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 userInfo = await parseUserData(user);
+
+ const api = `${API_HOST}/posts/getAllByUser?slug=${userInfo.slug}&take=${limit}`;
+
+ const timelineResponse = await customFetch
`)
+ .join('
');
+
+ const dataItem: DataItem = {
+ title: res.data.title || res.data.content,
+ pubDate: parseDate(res.data.createdAt),
+ link: `${HOST}/post/${item.postId}`,
+ description,
+ };
+
+ return dataItem;
+}
diff --git a/lib/routes/cartoonmad/namespace.ts b/lib/routes/cartoonmad/namespace.ts
index 3768ff781d78ac..d267db9e5f53a4 100644
--- a/lib/routes/cartoonmad/namespace.ts
+++ b/lib/routes/cartoonmad/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '動漫狂',
url: 'cartoonmad.com',
+ lang: 'zh-TW',
};
diff --git a/lib/routes/cas/cg/index.ts b/lib/routes/cas/cg/index.ts
index be466bad1e8622..df765efc1d17cd 100644
--- a/lib/routes/cas/cg/index.ts
+++ b/lib/routes/cas/cg/index.ts
@@ -27,8 +27,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 工作动态 | 科技成果转移转化亮点工作 |
- | -------- | ------------------------ |
- | zh | cgzhld |`,
+| -------- | ------------------------ |
+| zh | cgzhld |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cas/namespace.ts b/lib/routes/cas/namespace.ts
index 68c7a05f8bd51b..5cd8916c10051b 100644
--- a/lib/routes/cas/namespace.ts
+++ b/lib/routes/cas/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国科学院',
url: 'www.cas.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/casssp/namespace.ts b/lib/routes/casssp/namespace.ts
index c20708fdc076f7..1ba8855ad0f181 100644
--- a/lib/routes/casssp/namespace.ts
+++ b/lib/routes/casssp/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国科学学与科技政策研究会',
url: 'casssp.org.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/casssp/news.ts b/lib/routes/casssp/news.ts
index 8bf685857d7073..8c9c5f20bc335d 100644
--- a/lib/routes/casssp/news.ts
+++ b/lib/routes/casssp/news.ts
@@ -21,8 +21,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 通知公告 | 新闻动态 | 信息公开 | 时政要闻 |
- | -------- | -------- | -------- | -------- |
- | 3 | 2 | 92 | 93 |`,
+| -------- | -------- | -------- | -------- |
+| 3 | 2 | 92 | 93 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cast/index.ts b/lib/routes/cast/index.ts
index 264dda56e0afc8..dfad408465c596 100644
--- a/lib/routes/cast/index.ts
+++ b/lib/routes/cast/index.ts
@@ -60,19 +60,19 @@ export const route: Route = {
name: '通用',
maintainers: ['KarasuShin', 'TonyRL'],
handler,
- description: `:::tip
+ description: `::: tip
在路由末尾处加上 \`?limit=限制获取数目\` 来限制获取条目数量,默认值为\`10\`
- :::
+:::
- | 分类 | 编码 |
- | -------- | ---- |
- | 全景科协 | qjkx |
- | 智库 | zk |
- | 学术 | xs |
- | 科普 | kp |
- | 党建 | dj |
- | 数据 | sj |
- | 新闻 | xw |`,
+| 分类 | 编码 |
+| -------- | ---- |
+| 全景科协 | qjkx |
+| 智库 | zk |
+| 学术 | xs |
+| 科普 | kp |
+| 党建 | dj |
+| 数据 | sj |
+| 新闻 | xw |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cast/namespace.ts b/lib/routes/cast/namespace.ts
index 38c0e21b5aa509..8c1d0f7eb9a53e 100644
--- a/lib/routes/cast/namespace.ts
+++ b/lib/routes/cast/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国科学技术协会',
url: 'cast.org.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/catti/namespace.ts b/lib/routes/catti/namespace.ts
new file mode 100644
index 00000000000000..59c463f72d34ee
--- /dev/null
+++ b/lib/routes/catti/namespace.ts
@@ -0,0 +1,6 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '全国翻译专业资格水平考试 (CATTI)',
+ url: 'www.catticenter.com',
+};
diff --git a/lib/routes/catti/news.ts b/lib/routes/catti/news.ts
new file mode 100644
index 00000000000000..8d926c514115cc
--- /dev/null
+++ b/lib/routes/catti/news.ts
@@ -0,0 +1,120 @@
+import { DataItem, Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { parseDate } from '@/utils/parse-date';
+import { load } from 'cheerio';
+
+type NewsCategory = {
+ title: string;
+ description: string;
+};
+
+const NEWS_TYPES: Record更多分类
+
+#### [协会](https://www.cbpanet.com/dzp_xiehui.aspx)
+
+| [协会介绍](https://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=1) | [协会章程](https://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=2) | [理事会](https://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=3) | [内设机构](https://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=4) | [协会通知](https://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=5) | [协会活动](https://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=6) |
+| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
+| [1/1](https://rsshub.app/cbpanet/dzp_news/1/1) | [1/2](https://rsshub.app/cbpanet/dzp_news/1/2) | [1/3](https://rsshub.app/cbpanet/dzp_news/1/3) | [1/4](https://rsshub.app/cbpanet/dzp_news/1/4) | [1/5](https://rsshub.app/cbpanet/dzp_news/1/5) | [1/6](https://rsshub.app/cbpanet/dzp_news/1/6) |
+
+| [出版物](https://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=7) | [会员权利与义务](https://www.cbpanet.com/dzp_news.aspx?bigid=1&smallid=30) |
+| ---------------------------------------------------------------- | ------------------------------------------------------------------------- |
+| [1/7](https://rsshub.app/cbpanet/dzp_news/1/7) | [1/30](https://rsshub.app/cbpanet/dzp_news/1/30) |
+
+#### [行业资讯](https://www.cbpanet.com/dzp_news_list.aspx)
+
+| [国内资讯](https://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=8) | [海外资讯](https://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=9) | [企业新闻](https://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=10) | [行业资讯](https://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=11) | [热点聚焦](https://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=43) | [今日推荐](https://www.cbpanet.com/dzp_news.aspx?bigid=2&smallid=44) |
+| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- |
+| [2/8](https://rsshub.app/cbpanet/dzp_news/2/8) | [2/9](https://rsshub.app/cbpanet/dzp_news/2/9) | [2/10](https://rsshub.app/cbpanet/dzp_news/2/10) | [2/11](https://rsshub.app/cbpanet/dzp_news/2/11) | [2/43](https://rsshub.app/cbpanet/dzp_news/2/43) | [2/44](https://rsshub.app/cbpanet/dzp_news/2/44) |
+
+#### [原料信息](https://www.cbpanet.com/dzp_yuanliao.aspx)
+
+| [价格行情](https://www.cbpanet.com/dzp_news.aspx?bigid=3&smallid=12) | [分析预测](https://www.cbpanet.com/dzp_news.aspx?bigid=3&smallid=13) | [原料信息](https://www.cbpanet.com/dzp_news.aspx?bigid=3&smallid=40) | [热点聚焦](https://www.cbpanet.com/dzp_news.aspx?bigid=3&smallid=45) |
+| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- |
+| [3/12](https://rsshub.app/cbpanet/dzp_news/3/12) | [3/13](https://rsshub.app/cbpanet/dzp_news/3/13) | [3/40](https://rsshub.app/cbpanet/dzp_news/3/40) | [3/45](https://rsshub.app/cbpanet/dzp_news/3/45) |
+
+#### [法规标准](https://www.cbpanet.com/dzp_fagui.aspx)
+
+| [法规资讯](https://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=15) | [法律法规](https://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=16) | [国内标准](https://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=14) | [国外标准](https://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=17) | [法规聚焦](https://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=46) | [今日推荐](https://www.cbpanet.com/dzp_news.aspx?bigid=4&smallid=47) |
+| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- |
+| [4/15](https://rsshub.app/cbpanet/dzp_news/4/15) | [4/16](https://rsshub.app/cbpanet/dzp_news/4/16) | [4/14](https://rsshub.app/cbpanet/dzp_news/4/14) | [4/17](https://rsshub.app/cbpanet/dzp_news/4/17) | [4/46](https://rsshub.app/cbpanet/dzp_news/4/46) | [4/47](https://rsshub.app/cbpanet/dzp_news/4/47) |
+
+#### [技术专区](https://www.cbpanet.com/dzp_jishu.aspx)
+
+| [产品介绍](https://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=18) | [科技成果](https://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=19) | [学术论文](https://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=20) | [资料下载](https://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=21) | [专家](https://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=50) | [民间智库](https://www.cbpanet.com/dzp_news.aspx?bigid=5&smallid=57) |
+| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------- |
+| [5/18](https://rsshub.app/cbpanet/dzp_news/5/18) | [5/19](https://rsshub.app/cbpanet/dzp_news/5/19) | [5/20](https://rsshub.app/cbpanet/dzp_news/5/20) | [5/21](https://rsshub.app/cbpanet/dzp_news/5/21) | [5/50](https://rsshub.app/cbpanet/dzp_news/5/50) | [5/57](https://rsshub.app/cbpanet/dzp_news/5/57) |
+
+#### [豆制品消费指南](https://www.cbpanet.com/dzp_zhinan.aspx)
+
+| [膳食指南](https://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=22) | [营养成分](https://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=23) | [豆食菜谱](https://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=24) | [问与答](https://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=31) | [今日推荐](https://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=48) | [消费热点](https://www.cbpanet.com/dzp_news.aspx?bigid=6&smallid=53) |
+| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- |
+| [6/22](https://rsshub.app/cbpanet/dzp_news/6/22) | [6/23](https://rsshub.app/cbpanet/dzp_news/6/23) | [6/24](https://rsshub.app/cbpanet/dzp_news/6/24) | [6/31](https://rsshub.app/cbpanet/dzp_news/6/31) | [6/48](https://rsshub.app/cbpanet/dzp_news/6/48) | [6/53](https://rsshub.app/cbpanet/dzp_news/6/53) |
+
+#### [营养与健康](https://www.cbpanet.com/dzp_yingyang.aspx)
+
+| [大豆营养概况](https://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=25) | [大豆食品和人类健康](https://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=26) | [世界豆类日,爱豆大行动](https://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=27) | [谣言粉碎机](https://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=29) | [最新资讯](https://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=41) | [专家视点](https://www.cbpanet.com/dzp_news.aspx?bigid=7&smallid=49) |
+| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- |
+| [7/25](https://rsshub.app/cbpanet/dzp_news/7/25) | [7/26](https://rsshub.app/cbpanet/dzp_news/7/26) | [7/27](https://rsshub.app/cbpanet/dzp_news/7/27) | [7/29](https://rsshub.app/cbpanet/dzp_news/7/29) | [7/41](https://rsshub.app/cbpanet/dzp_news/7/41) | [7/49](https://rsshub.app/cbpanet/dzp_news/7/49) |
+
+更多分类
+
+#### [会员之家](https://www.cccmc.org.cn/hyzj)
+
+| [会员之声](https://www.cccmc.org.cn/hyzj/hyzs/) | [会员动态](https://www.cccmc.org.cn/hyzj/hydt/) | [会员推介](https://www.cccmc.org.cn/hyzj/hytj/) |
+| ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- |
+| [hyzj/hyzs](https://rsshub.app/cccmc/hyzj/hyzs) | [hyzj/hydt](https://rsshub.app/cccmc/hyzj/hydt) | [hyzj/hytj](https://rsshub.app/cccmc/hyzj/hytj) |
+
+#### [政策法规](https://www.cccmc.org.cn/zcfg)
+
+| [综合政策](https://www.cccmc.org.cn/zcfg/zhzc/) | [国内贸易](https://www.cccmc.org.cn/zcfg/gnmy/) | [对外贸易](https://www.cccmc.org.cn/zcfg/dwmy/) | [投资合作](https://www.cccmc.org.cn/zcfg/tzhz/) |
+| ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- |
+| [zcfg/zhzc](https://rsshub.app/cccmc/zcfg/zhzc) | [zcfg/gnmy](https://rsshub.app/cccmc/zcfg/gnmy) | [zcfg/dwmy](https://rsshub.app/cccmc/zcfg/dwmy) | [zcfg/tzhz](https://rsshub.app/cccmc/zcfg/tzhz) |
+
+#### [行业资讯](https://www.cccmc.org.cn/hyzx)
+
+| [统计分析](https://www.cccmc.org.cn/hyzx/tjfx/) | [石油化工](https://www.cccmc.org.cn/hyzx/syhg/) | [金属矿产](https://www.cccmc.org.cn/hyzx/jskc/) | [五金建材](https://www.cccmc.org.cn/hyzx/wjjc/) |
+| ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- |
+| [hyzx/tjfx](https://rsshub.app/cccmc/hyzx/tjfx) | [hyzx/syhg](https://rsshub.app/cccmc/hyzx/syhg) | [hyzx/jskc](https://rsshub.app/cccmc/hyzx/jskc) | [hyzx/wjjc](https://rsshub.app/cccmc/hyzx/wjjc) |
+
+#### [商业机会](https://www.cccmc.org.cn/syjh/)+
+
+| [供应信息](https://www.cccmc.org.cn/syjh/gyxx/) | [需求信息](https://www.cccmc.org.cn/syjh/xqxx/) | [合作信息](https://www.cccmc.org.cn/syjh/hzxx/) |
+| ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- |
+| [syjh/gyxx](https://rsshub.app/cccmc/syjh/gyxx) | [syjh/xqxx](https://rsshub.app/cccmc/syjh/xqxx) | [syjh/hzxx](https://rsshub.app/cccmc/syjh/hzxx) |
+
+#### [商会党建](https://www.cccmc.org.cn/shdj)
+
+| [党群动态](https://www.cccmc.org.cn/shdj/dqdt/) | [党内法规](https://www.cccmc.org.cn/shdj/dnfg/) | [青年工作](https://www.cccmc.org.cn/shdj/qngz/) |
+| ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- |
+| [shdj/dqdt](https://rsshub.app/cccmc/shdj/dqdt) | [shdj/dnfg](https://rsshub.app/cccmc/shdj/dnfg) | [shdj/qngz](https://rsshub.app/cccmc/shdj/qngz) |
+{{ intro }}
+{{ /if }}
+
+{{ if description }}
+ {{@ description }}
+{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/ccnu/namespace.ts b/lib/routes/ccnu/namespace.ts
index dd46468ac4b5e6..e13e8112acadd8 100644
--- a/lib/routes/ccnu/namespace.ts
+++ b/lib/routes/ccnu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '华中师范大学',
url: 'ccnu.91wllm.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/ccreports/namespace.ts b/lib/routes/ccreports/namespace.ts
index 309fd8cbe28d2f..19ebbfbf5fc26d 100644
--- a/lib/routes/ccreports/namespace.ts
+++ b/lib/routes/ccreports/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '消费者报道',
url: 'www.ccreports.com.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cctv/category.ts b/lib/routes/cctv/category.ts
index 8c416634ff375c..f1f854bb2864f4 100644
--- a/lib/routes/cctv/category.ts
+++ b/lib/routes/cctv/category.ts
@@ -2,6 +2,7 @@ import { Route } from '@/types';
import getMzzlbg from './utils/mzzlbg';
import xinwen1j1 from './utils/xinwen1j1';
import getNews from './utils/news';
+import getXWLB from './xwlb';
export const route: Route = {
path: '/:category',
@@ -25,24 +26,27 @@ export const route: Route = {
maintainers: ['idealclover', 'xyqfer'],
handler,
description: `| 新闻 | 国内 | 国际 | 社会 | 法治 | 文娱 | 科技 | 生活 | 教育 | 每周质量报告 | 新闻 1+1 |
- | ---- | ----- | ----- | ------- | ---- | ---- | ---- | ---- | ---- | ------------ | --------- |
- | news | china | world | society | law | ent | tech | life | edu | mzzlbg | xinwen1j1 |`,
+| ---- | ----- | ----- | ------- | ---- | ---- | ---- | ---- | ---- | ------------ | --------- |
+| news | china | world | society | law | ent | tech | life | edu | mzzlbg | xinwen1j1 |`,
};
async function handler(ctx) {
const category = ctx.req.param('category');
- let responseData;
- if (category === 'mzzlbg') {
- // 每周质量报告
- responseData = await getMzzlbg();
- } else if (category === 'xinwen1j1') {
- // 新闻1+1
- responseData = await xinwen1j1();
- } else {
- // 央视新闻
- responseData = await getNews(category);
- }
+ switch (category) {
+ case 'mzzlbg':
+ // 每周质量报告
+ return await getMzzlbg();
+
+ case 'xinwen1j1':
+ // 新闻1+1
+ return await xinwen1j1();
- return responseData;
+ case 'xwlb':
+ return await getXWLB();
+
+ default:
+ // 央视新闻
+ return await getNews(category);
+ }
}
diff --git a/lib/routes/cctv/lm.ts b/lib/routes/cctv/lm.ts
index 5809f3424a6ea7..7294c74c01d9e9 100644
--- a/lib/routes/cctv/lm.ts
+++ b/lib/routes/cctv/lm.ts
@@ -28,16 +28,16 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 焦点访谈 | 等着我 | 今日说法 | 开讲啦 |
- | -------- | ------ | -------- | ------ |
- | jdft | dzw | jrsf | kjl |
+| -------- | ------ | -------- | ------ |
+| jdft | dzw | jrsf | kjl |
- | 正大综艺 | 经济半小时 | 第一动画乐园 |
- | -------- | ---------- | ------------ |
- | zdzy | jjbxs | dydhly |
+| 正大综艺 | 经济半小时 | 第一动画乐园 |
+| -------- | ---------- | ------------ |
+| zdzy | jjbxs | dydhly |
- :::tip
+::: tip
更多栏目请看 [这里](https://tv.cctv.com/lm)
- :::`,
+:::`,
};
async function handler(ctx) {
diff --git a/lib/routes/cctv/namespace.ts b/lib/routes/cctv/namespace.ts
index 766fb0b88da296..1c85ef01b5780b 100644
--- a/lib/routes/cctv/namespace.ts
+++ b/lib/routes/cctv/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '央视新闻',
url: 'news.cctv.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cctv/xwlb.ts b/lib/routes/cctv/xwlb.ts
index d0218dbf37887b..121f94b6816628 100644
--- a/lib/routes/cctv/xwlb.ts
+++ b/lib/routes/cctv/xwlb.ts
@@ -9,10 +9,22 @@ import customParseFormat from 'dayjs/plugin/customParseFormat';
dayjs.extend(customParseFormat);
export const route: Route = {
- path: '/xwlb',
+ path: '/:site/:category/:name',
categories: ['traditional-media'],
- example: '/cctv/xwlb',
- parameters: {},
+ example: '/cctv/tv/lm/xwlb',
+ parameters: {
+ site: "站点, 可选值如'tv', 既'央视节目'",
+ category: "分类名, 官网对应分类, 当前可选值'lm', 既'栏目大全'",
+ name: {
+ description: "栏目名称, 可在对应栏目页面 URL 中找到, 可选值如'xwlb',既'新闻联播'",
+ options: [
+ {
+ value: 'xwlb',
+ label: '新闻联播',
+ },
+ ],
+ },
+ },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -33,12 +45,21 @@ export const route: Route = {
description: `新闻联播内容摘要。`,
};
-async function handler() {
+async function handler(ctx) {
+ const { site, category, name } = ctx.req.param();
+ let responseData;
+ if (site === 'tv' && category === 'lm' && name === 'xwlb') {
+ responseData = await getXWLB();
+ }
+ return responseData;
+}
+
+const getXWLB = async () => {
const res = await got({ method: 'get', url: 'https://tv.cctv.com/lm/xwlb/' });
const $ = load(res.data);
// 解析最新一期新闻联播的日期
const latestDate = dayjs($('.rilititle p').text(), 'YYYY-MM-DD');
- const count = [];
+ const count: number[] = [];
for (let i = 0; i < 20; i++) {
count.push(i);
}
@@ -49,13 +70,13 @@ async function handler() {
const item = {
title: `新闻联播 ${newsDate.format('YYYY/MM/DD')}`,
link: url,
- pubDate: timezone(parseDate(newsDate), +8),
+ pubDate: timezone(parseDate(newsDate.format()), +8),
description: await cache.tryGet(url, async () => {
const res = await got(url);
const content = load(res.data);
- const list = [];
- content('body li').map((i, e) => {
- e = content(e);
+ 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();
@@ -74,4 +95,5 @@ async function handler() {
link: 'http://tv.cctv.com/lm/xwlb/',
item: resultItems,
};
-}
+};
+export default getXWLB;
diff --git a/lib/routes/cde/index.ts b/lib/routes/cde/index.ts
index a3589cf491ad49..f142f95f0dee9a 100644
--- a/lib/routes/cde/index.ts
+++ b/lib/routes/cde/index.ts
@@ -94,19 +94,19 @@ export const route: Route = {
handler,
description: `- 频道
- | 新闻中心 | 政策法规 |
- | :------: | :------: |
- | news | policy |
+| 新闻中心 | 政策法规 |
+| :------: | :------: |
+| news | policy |
- 类别
- | 新闻中心 | 政务新闻 | 要闻导读 | 图片新闻 | 工作动态 |
- | :------: | :------: | :------: | :------: | :------: |
- | | zwxw | ywdd | tpxw | gzdt |
+| 新闻中心 | 政务新闻 | 要闻导读 | 图片新闻 | 工作动态 |
+| :------: | :------: | :------: | :------: | :------: |
+| | zwxw | ywdd | tpxw | gzdt |
- | 政策法规 | 法律法规 | 中心规章 |
- | :------: | :------: | :------: |
- | | flfg | zxgz |`,
+| 政策法规 | 法律法规 | 中心规章 |
+| :------: | :------: | :------: |
+| | flfg | zxgz |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cde/namespace.ts b/lib/routes/cde/namespace.ts
index 24497b5e7034df..074888104d2e82 100644
--- a/lib/routes/cde/namespace.ts
+++ b/lib/routes/cde/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '国家药品审评网站',
url: 'www.cde.org.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cde/xxgk.ts b/lib/routes/cde/xxgk.ts
index 6504f56c9d27e4..75208249d4aa4e 100644
--- a/lib/routes/cde/xxgk.ts
+++ b/lib/routes/cde/xxgk.ts
@@ -67,8 +67,8 @@ export const route: Route = {
maintainers: ['TonyRL'],
handler,
description: `| 优先审评公示 | 突破性治疗公示 | 临床试验默示许可 |
- | :--------------: | :--------------: | :--------------: |
- | priorityApproval | breakthroughCure | cliniCal |`,
+| :--------------: | :--------------: | :--------------: |
+| priorityApproval | breakthroughCure | cliniCal |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cde/zdyz.ts b/lib/routes/cde/zdyz.ts
index e928bb177e460b..a3166acf5780c3 100644
--- a/lib/routes/cde/zdyz.ts
+++ b/lib/routes/cde/zdyz.ts
@@ -55,8 +55,8 @@ export const route: Route = {
maintainers: ['TonyRL'],
handler,
description: `| 发布通告 | 征求意见 |
- | :-----------: | :---------: |
- | domesticGuide | opinionList |`,
+| :-----------: | :---------: |
+| domesticGuide | opinionList |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cdi/index.ts b/lib/routes/cdi/index.ts
index 9f4e082bd6648b..93338d72524ad0 100644
--- a/lib/routes/cdi/index.ts
+++ b/lib/routes/cdi/index.ts
@@ -22,8 +22,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 樊纲观点 | 综研国策 | 综研观察 | 综研专访 | 综研视点 | 银湖新能源 |
- | -------- | -------- | -------- | -------- | -------- | ---------- |
- | 102 | 152 | 150 | 153 | 154 | 151 |`,
+| -------- | -------- | -------- | -------- | -------- | ---------- |
+| 102 | 152 | 150 | 153 | 154 | 151 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cdi/namespace.ts b/lib/routes/cdi/namespace.ts
index f976a667f7e950..93130b4bdcafc8 100644
--- a/lib/routes/cdi/namespace.ts
+++ b/lib/routes/cdi/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '国家高端智库 / 综合开发研究院',
url: 'cdi.com.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cdu/jwgg.ts b/lib/routes/cdu/jwgg.ts
new file mode 100644
index 00000000000000..dcd2f2be0fa2bd
--- /dev/null
+++ b/lib/routes/cdu/jwgg.ts
@@ -0,0 +1,79 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+
+export const route: Route = {
+ path: '/jwgg',
+ categories: ['university'],
+ example: '/cdu/jwgg',
+ parameters: {},
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['jw.cdu.edu.cn/'],
+ },
+ ],
+ name: '教务处通知公告',
+ maintainers: ['uuwor'],
+ handler,
+ url: 'jw.cdu.edu.cn/',
+};
+
+async function handler() {
+ const url = 'https://jw.cdu.edu.cn/jwgg.htm'; // 数据来源网页(待提取网页)
+ const response = await got.get(url);
+ const data = response.data;
+ const $ = load(data);
+ const list = $('.ListTable.dataTable.no-footer tbody tr[role="row"].odd')
+ .slice(0, 10)
+ .toArray()
+ .map((e) => {
+ const element = $(e);
+ const title = element.find('tr.odd a').text().trim(); /* 1.选择器 tr.odd a:这个选择器查找具有 class="odd" 的 元素下的 标签。
+ 2..text():该方法获取选中元素的文本内容。
+ 3..trim():用于去掉字符串前后的空格,确保得到干净的文本。*/
+ const link = element.find('tr.odd a').attr('href');
+ const date = element
+ .find('tr.odd td.columnDate')
+ .text()
+ .match(/\d{4}-\d{2}-\d{2}/);
+ const pubDate = timezone(parseDate(date), 8);
+
+ return {
+ title,
+ link: 'https://jw.cdu.edu.cn/' + link,
+ author: '成都大学教务处通知公告',
+ pubDate,
+ };
+ });
+
+ const result = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const itemReponse = await got.get(item.link);
+ const data = itemReponse.data;
+ const itemElement = load(data);
+
+ item.description = itemElement('.v_news_content').html();
+
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: '成大教务处通知公告',
+ link: url,
+ item: result,
+ };
+}
diff --git a/lib/routes/cdu/namespace.ts b/lib/routes/cdu/namespace.ts
new file mode 100644
index 00000000000000..cefdf80b32d652
--- /dev/null
+++ b/lib/routes/cdu/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '成都大学',
+ url: 'www.cdu.edu.cn',
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/cdu/tzggcdunews.ts b/lib/routes/cdu/tzggcdunews.ts
new file mode 100644
index 00000000000000..c4f299dc4abcb3
--- /dev/null
+++ b/lib/routes/cdu/tzggcdunews.ts
@@ -0,0 +1,80 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+
+export const route: Route = {
+ path: '/tzggcdunews',
+ categories: ['university'],
+ example: '/cdu/tzggcdunews',
+ parameters: {},
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['news.cdu.edu.cn/'],
+ },
+ ],
+ name: '通知公告',
+ maintainers: ['uuwor'],
+ handler,
+ url: 'news.cdu.edu.cn/',
+};
+
+async function handler() {
+ const baseUrl = 'https://news.cdu.edu.cn';
+ const url = `${baseUrl}/tzgg.htm`;
+ const response = await got.get(url);
+ const $ = load(response.data);
+
+ const list = $('.row-f1 ul.ul-mzw-news-a2 li a.con')
+ .slice(0, 10)
+ .toArray()
+ .map((item) => {
+ const element = $(item);
+ // 优先使用title属性内容,避免内容被截断
+ const title = element.attr('title') || element.find('.tit').text().trim();
+ const link = element.attr('href');
+ const dateText = element.find('.date').text().trim();
+ const pubDate = timezone(parseDate(dateText), 8);
+
+ return {
+ title,
+ // 处理相对路径链接
+ link: link.startsWith('http') ? link : new URL(link, baseUrl).href,
+ pubDate,
+ author: '成都大学官网通知公告',
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const response = await got.get(item.link);
+ const $ = load(response.data);
+
+ // 清理无关内容并提取正文
+ const content = $('.v_news_content');
+ // 移除版权声明等无关元素
+ content.find('*[style*="text-align: right"]').remove();
+
+ item.description = content.html();
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: '成都大学官网-通知公告',
+ link: url,
+ item: items,
+ };
+}
diff --git a/lib/routes/cdzjryb/namespace.ts b/lib/routes/cdzjryb/namespace.ts
index 26c405bc0113b5..7245dd0ffb3526 100644
--- a/lib/routes/cdzjryb/namespace.ts
+++ b/lib/routes/cdzjryb/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '成都住建蓉 e 办',
url: 'zw.cdzjryb.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/ce/district.ts b/lib/routes/ce/district.ts
new file mode 100644
index 00000000000000..1f2b8180eba161
--- /dev/null
+++ b/lib/routes/ce/district.ts
@@ -0,0 +1,91 @@
+import { Route, ViewType } from '@/types';
+import cache from '@/utils/cache';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+import { load } from 'cheerio';
+import { ofetch } from 'ofetch';
+
+export const route: Route = {
+ path: '/district/:category?',
+ name: '地方经济',
+ url: 'district.ce.cn',
+ maintainers: ['cscnk52'],
+ handler,
+ example: '/ce/district',
+ parameters: { category: '栏目标识,默认为 roll(即时新闻)' },
+ description: `| 即时新闻 | 经济动态 | 独家视角 | 专题 | 数说地方 | 地方播报 | 专稿 | 港澳台 |
+|----------|----------|----------|------|----------|----------|------|--------|
+| roll | jjdt | poll | ch | ssdf | dfbb | zg | gat |`,
+ categories: ['traditional-media'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ supportRadar: true,
+ },
+ radar: [
+ {
+ source: ['district.ce.cn/newarea/:category/index.shtml'],
+ target: '/district/:category?',
+ },
+ {
+ source: ['district.ce.cn/newarea/:category'],
+ target: '/district/:category?',
+ },
+ {
+ source: ['district.ce.cn'],
+ target: '/district',
+ },
+ ],
+ view: ViewType.Articles,
+};
+
+async function handler(ctx) {
+ const rootUrl = 'http://district.ce.cn/';
+ const { category = 'roll' } = ctx.req.param();
+ const url = `${rootUrl}newarea/${category}/index.shtml`;
+ const GB2312Response = await ofetch(url, { responseType: 'arrayBuffer' });
+
+ // originally site use gb2312 encoding
+ const response = new TextDecoder('gb2312').decode(new Uint8Array(GB2312Response));
+ const $ = load(response);
+
+ const bigTitle = $('div.channl a').eq(1).attr('title');
+
+ const list = $('div.sec_left li')
+ .toArray()
+ .map((e) => {
+ const element = $(e);
+ const title = element.find('a').text().trim();
+ const link = new URL(element.find('a').attr('href'), url).href;
+ return {
+ title,
+ link,
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const GB2312Response = await ofetch(item.link, { responseType: 'arrayBuffer' });
+ const response = new TextDecoder('gb2312').decode(new Uint8Array(GB2312Response));
+ const $ = load(response);
+
+ const pubDateText = $('span#articleTime').text().trim();
+ item.pubDate = timezone(parseDate(pubDateText, 'YYYY年MM月DD日 HH:mm'), +8);
+
+ item.description = $('div.TRS_Editor').html();
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: `中国经济网地方经济 - ${bigTitle}`,
+ link: url,
+ item: items,
+ };
+}
diff --git a/lib/routes/ce/namespace.ts b/lib/routes/ce/namespace.ts
new file mode 100644
index 00000000000000..94dfbe1af642c2
--- /dev/null
+++ b/lib/routes/ce/namespace.ts
@@ -0,0 +1,8 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '中国经济网',
+ url: 'www.ce.cn',
+ categories: ['traditional-media'],
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/cebbank/history.ts b/lib/routes/cebbank/history.ts
index 352814f9a10f00..35666f8b836f01 100644
--- a/lib/routes/cebbank/history.ts
+++ b/lib/routes/cebbank/history.ts
@@ -31,9 +31,9 @@ export const route: Route = {
#### 历史牌价 {#zhong-guo-guang-da-yin-hang-wai-hui-pai-jia-li-shi-pai-jia}
- | 美元 | 英镑 | 港币 | 瑞士法郎 | 瑞典克郎 | 丹麦克郎 | 挪威克郎 | 日元 | 加拿大元 | 澳大利亚元 | 新加坡元 | 欧元 | 澳门元 | 泰国铢 | 新西兰元 | 韩圆 |
- | ---- | ---- | ---- | -------- | -------- | -------- | -------- | ---- | -------- | ---------- | -------- | ---- | ------ | ------ | -------- | ---- |
- | usd | gbp | hkd | chf | sek | dkk | nok | jpy | cad | aud | sgd | eur | mop | thb | nzd | krw |`,
+| 美元 | 英镑 | 港币 | 瑞士法郎 | 瑞典克郎 | 丹麦克郎 | 挪威克郎 | 日元 | 加拿大元 | 澳大利亚元 | 新加坡元 | 欧元 | 澳门元 | 泰国铢 | 新西兰元 | 韩圆 |
+| ---- | ---- | ---- | -------- | -------- | -------- | -------- | ---- | -------- | ---------- | -------- | ---- | ------ | ------ | -------- | ---- |
+| usd | gbp | hkd | chf | sek | dkk | nok | jpy | cad | aud | sgd | eur | mop | thb | nzd | krw |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cebbank/namespace.ts b/lib/routes/cebbank/namespace.ts
index 2bafdede77b80c..360b3424ee677c 100644
--- a/lib/routes/cebbank/namespace.ts
+++ b/lib/routes/cebbank/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国光大银行',
url: 'cebbank.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/ceph/blog.ts b/lib/routes/ceph/blog.ts
new file mode 100644
index 00000000000000..7c5e1dc9a2c2dd
--- /dev/null
+++ b/lib/routes/ceph/blog.ts
@@ -0,0 +1,72 @@
+import { Data, Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+import { Context } from 'hono';
+
+export const route: Route = {
+ path: '/blog/:topic?',
+ categories: ['blog'],
+ example: '/ceph/blog/a11y',
+ parameters: {
+ category: 'filter blog post by category, return all posts if not specified',
+ },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['ceph.io/'],
+ },
+ ],
+ name: 'Blog',
+ maintainers: ['pandada8'],
+ handler,
+ url: 'ceph.io',
+};
+
+async function handler(ctx: Context): Promise {
+ const { category } = ctx.req.param();
+ const url = category ? `https://ceph.io/en/news/blog/category/${category}/` : 'https://ceph.io/en/news/blog/';
+ const response = await got.get(url);
+ const data = response.data;
+ const $ = load(data);
+ const list = $('#main .section li')
+ .toArray()
+ .map((e) => {
+ const element = $(e);
+ const title = element.find('a').text().trim();
+ const pubDate = parseDate(element.find('time').attr('datetime'));
+ return {
+ title,
+ link: new URL(element.find('a').attr('href'), 'https://ceph.io').href,
+ pubDate,
+ };
+ });
+
+ const result = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const itemReponse = await got.get(item.link);
+ const data = itemReponse.data;
+ const item$ = load(data);
+
+ item.author = item$('#main section > div:nth-child(1) span').text().trim();
+ item.description = item$('#main section > div:nth-child(2) > div').html();
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: 'Ceph Blog',
+ link: url,
+ item: result,
+ };
+}
diff --git a/lib/routes/ceph/namespace.ts b/lib/routes/ceph/namespace.ts
new file mode 100644
index 00000000000000..0313877bf90be7
--- /dev/null
+++ b/lib/routes/ceph/namespace.ts
@@ -0,0 +1,8 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Ceph',
+ url: 'ceph.io',
+ description: 'Ceph is an open source distributed storage system designed to evolve with data.',
+ lang: 'en',
+};
diff --git a/lib/routes/cfachina/analygarden.ts b/lib/routes/cfachina/analygarden.ts
index c10f220c4dca63..0995860ec5e965 100644
--- a/lib/routes/cfachina/analygarden.ts
+++ b/lib/routes/cfachina/analygarden.ts
@@ -26,8 +26,8 @@ export const route: Route = {
maintainers: ['TonyRL'],
handler,
description: `| 有色金属类 | 黑色金属类 | 能源化工类 | 贵金属类 | 农产品类 | 金融类 | 指数类 |
- | ---------- | ---------- | ---------- | -------- | -------- | ------ | ------ |
- | ysjsl | hsjsl | nyhgl | gjsl | ncpl | jrl | zsl |`,
+| ---------- | ---------- | ---------- | -------- | -------- | ------ | ------ |
+| ysjsl | hsjsl | nyhgl | gjsl | ncpl | jrl | zsl |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cfachina/namespace.ts b/lib/routes/cfachina/namespace.ts
index 28aa653fc42a02..74b9dc44765640 100644
--- a/lib/routes/cfachina/namespace.ts
+++ b/lib/routes/cfachina/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国期货业协会',
url: 'cfachina.org',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cffex/announcement.ts b/lib/routes/cffex/announcement.ts
new file mode 100644
index 00000000000000..f9d449335ed79b
--- /dev/null
+++ b/lib/routes/cffex/announcement.ts
@@ -0,0 +1,73 @@
+import { DataItem, Route } from '@/types';
+import ofetch from '@/utils/ofetch';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+import cache from '@/utils/cache';
+
+export const route: Route = {
+ path: '/announcement',
+ name: '交易所公告',
+ url: 'www.cffex.com.cn',
+ maintainers: ['ChenXiangcheng1'],
+ example: '/cffex/announcement',
+ parameters: {},
+ description: '',
+ categories: ['government'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['cffex.com.cn'],
+ target: '/announcement',
+ },
+ ],
+ handler,
+};
+
+async function handler(): Promise<{ title: string; link: string; item: DataItem[] }> {
+ const baseUrl = 'http://www.cffex.com.cn';
+ const homeUrl = `${baseUrl}/jystz`;
+ const response = await ofetch(homeUrl);
+
+ // 使用 Cheerio 选择器解析 HTML
+ const $ = load(response);
+ const list = $('div.notice_list li')
+ .toArray()
+ .map((item) => {
+ item = $(item); // (Element) -> LoadedCheerio
+ const titleEle = $(item).find('a').first();
+ const dateEle = $(item).find('a').eq(1);
+
+ return {
+ title: titleEle.text().trim(),
+ link: `${baseUrl}${titleEle.attr('href')}`,
+ pubDate: timezone(parseDate(dateEle.text(), 'YYYY-MM-DD'), +8),
+ };
+ });
+
+ // (Promise) -> Promise
+ const items = await Promise.all(
+ // (Promise|null) -> Promise|null
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const response = await ofetch(item.link);
+ const $ = load(response);
+ item.description = $('div.jysggnr div.nan p').eq(1)?.html();
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: '中国金融期货交易所 - 交易所公告',
+ link: homeUrl,
+ item: items,
+ };
+}
diff --git a/lib/routes/cffex/namespace.ts b/lib/routes/cffex/namespace.ts
new file mode 100644
index 00000000000000..1c51fc9df2659f
--- /dev/null
+++ b/lib/routes/cffex/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '中国金融期货交易所',
+ url: 'cffex.com.cn',
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/cfmmc/namespace.ts b/lib/routes/cfmmc/namespace.ts
index b99decd30df5a1..8091132c2806da 100644
--- a/lib/routes/cfmmc/namespace.ts
+++ b/lib/routes/cfmmc/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国期货市场监控中心',
url: 'cfmmc.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cfr/index.ts b/lib/routes/cfr/index.ts
new file mode 100644
index 00000000000000..a1d128de4ad3d2
--- /dev/null
+++ b/lib/routes/cfr/index.ts
@@ -0,0 +1,58 @@
+import type { Data, Route } from '@/types';
+import type { Context } from 'hono';
+import ofetch from '@/utils/ofetch';
+import { load } from 'cheerio';
+import { asyncPoolAll, getDataItem } from './utils';
+
+export const route: Route = {
+ path: '/:category/:subCategory?',
+ categories: ['traditional-media'],
+ parameters: {
+ category: 'category, find it in the URL',
+ subCategory: 'sub-category, find it in the URL',
+ },
+ example: '/cfr/asia',
+ name: 'News',
+ maintainers: ['KarasuShin'],
+ handler,
+ radar: [
+ {
+ source: ['www.cfr.org/:category', 'www.cfr.org/:category/:subCategory'],
+ target: '/:category/:subCategory?',
+ },
+ ],
+ features: {
+ antiCrawler: true,
+ },
+};
+
+async function handler(ctx: Context): Promise {
+ const { category, subCategory } = ctx.req.param();
+
+ const origin = 'https://www.cfr.org';
+ let link = `${origin}/${category}`;
+ if (subCategory) {
+ link += `/${subCategory}`;
+ }
+ const res = await ofetch(link);
+
+ const $ = load(res);
+
+ const selectorMap: {
+ [key: string]: string;
+ } = {
+ podcasts: '.episode-content__title a',
+ blog: '.card-series__content-link',
+ 'books-reports': '.card-article__link',
+ };
+
+ const listSelector = selectorMap[category] ?? '.card-article-large__link';
+
+ const items = await asyncPoolAll(5, $(listSelector).toArray(), async (item) => await getDataItem($(item).attr('href')!));
+
+ return {
+ title: $('head title').text().replace(' | Council on Foreign Relations', ''),
+ link,
+ item: items,
+ };
+}
diff --git a/lib/routes/cfr/namespace.ts b/lib/routes/cfr/namespace.ts
new file mode 100644
index 00000000000000..939154e0cb5a5f
--- /dev/null
+++ b/lib/routes/cfr/namespace.ts
@@ -0,0 +1,7 @@
+import { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Council on Foreign Relations',
+ url: 'www.cfr.org',
+ lang: 'en',
+};
diff --git a/lib/routes/cfr/types.ts b/lib/routes/cfr/types.ts
new file mode 100644
index 00000000000000..c81cc58a2997d0
--- /dev/null
+++ b/lib/routes/cfr/types.ts
@@ -0,0 +1,30 @@
+export interface LinkData {
+ '@context': string;
+ '@graph': {
+ '@type': string;
+ headline: string;
+ name: string;
+ about: string;
+ description: string;
+ image: {
+ '@type': string;
+ representativeOfPage: string;
+ url: string;
+ };
+ datePublished: string;
+ dateModified: string;
+ author: {
+ '@type': string;
+ name: string;
+ url: string;
+ };
+ }[];
+}
+
+export interface VideoSetup {
+ techOrder: string[];
+ sources: {
+ type: string;
+ src: string;
+ }[];
+}
diff --git a/lib/routes/cfr/utils.ts b/lib/routes/cfr/utils.ts
new file mode 100644
index 00000000000000..7205f690b581e4
--- /dev/null
+++ b/lib/routes/cfr/utils.ts
@@ -0,0 +1,284 @@
+import { type Cheerio, type CheerioAPI, type Element, load } from 'cheerio';
+import ofetch from '@/utils/ofetch';
+import type { DataItem } from '@/types';
+import { parseDate } from '@/utils/parse-date';
+import cache from '@/utils/cache';
+import type { LinkData, VideoSetup } from './types';
+import asyncPool from 'tiny-async-pool';
+
+export function getDataItem(href: string) {
+ const origin = 'https://www.cfr.org';
+ const link = `${origin}${href}`;
+
+ return cache.tryGet(link, async () => {
+ const prefix = href?.split('/')[1];
+ const res = await ofetch(link);
+ const $ = load(res);
+
+ let dataItem: DataItem;
+
+ switch (prefix) {
+ case 'article':
+ dataItem = parseArticle($);
+ break;
+ case 'blog':
+ dataItem = parseBlog($);
+ break;
+ case 'book':
+ dataItem = parseBook($);
+ break;
+ case 'conference-calls':
+ dataItem = parseConferenceCalls($);
+ break;
+ case 'event':
+ dataItem = parseEvent($);
+ break;
+ case 'backgrounder':
+ dataItem = parseBackgrounder($);
+ break;
+ case 'podcasts':
+ dataItem = parsePodcasts($);
+ break;
+ case 'task-force-report':
+ dataItem = parseTaskForceReport($);
+ break;
+ case 'timeline':
+ dataItem = parseTimeline($);
+ break;
+ case 'video':
+ dataItem = parseVideo($);
+ break;
+ default:
+ dataItem = parseDefault($);
+ }
+
+ return {
+ ...dataItem,
+ link,
+ };
+ }) as Promise
${description}`;
+ }
+ return {
+ title: linkData?.title ?? $('.article-header__title').text(),
+ pubDate: linkData?.pubDate,
+ description,
+ };
+}
+
+function parseBlog($: CheerioAPI): DataItem {
+ const linkData = parseLinkData($);
+ let description = parseDescription($('.body-content'), $);
+ const figure = $('.article-header-blog__figure');
+ if (figure.length) {
+ description = `
${description}`;
+ }
+ return {
+ title: linkData?.title ?? $('.article-header-blog__title').text(),
+ pubDate: linkData?.pubDate,
+ description,
+ };
+}
+
+function parseBook($: CheerioAPI): DataItem {
+ const linkData = parseLinkData($);
+ let description = parseDescription($('.body-content'), $);
+ const sectionTop = $('.article-header__section-top');
+ description = `${sectionTop.html()}
${description}`;
+
+ return {
+ title: linkData?.title ?? $('.article-header__title').text(),
+ pubDate: linkData?.pubDate,
+ description,
+ };
+}
+
+function parseConferenceCalls($: CheerioAPI): DataItem {
+ const linkData = parseLinkData($);
+ const description = parseDescription($('.podcast-body').last(), $);
+ return {
+ title: linkData?.title ?? $('head title').text(),
+ pubDate: linkData?.pubDate,
+ description,
+ };
+}
+
+function parseEvent($: CheerioAPI): DataItem {
+ const linkData = parseLinkData($);
+ let description = parseDescription($('.body-content'), $);
+ const videoIfame = getVideoIframe($('.msp-event-video'));
+ if (videoIfame) {
+ description = `${videoIfame}
${description}`;
+ }
+
+ return {
+ title: linkData?.title ?? $('.msp-event-header-past__title').text(),
+ pubDate: linkData?.pubDate,
+ description,
+ };
+}
+
+function parseBackgrounder($: CheerioAPI): DataItem {
+ const linkData = parseLinkData($);
+ let description = parseDescription($('.main-wrapper__article-body .body-content'), $);
+ const summary = $('.main-wrapper__article-body .summary').html();
+ if (summary) {
+ description = `${summary}
${description}`;
+ }
+ const figure = $('.article-header-backgrounder__figure');
+ if (figure.length) {
+ description = `
${description}`;
+ }
+
+ return {
+ title: linkData?.title ?? $('.article-header-backgrounder__title').text(),
+ pubDate: linkData?.pubDate,
+ description,
+ };
+}
+
+function parsePodcasts($: CheerioAPI): DataItem {
+ const linkData = parseLinkData($);
+ let description = $('.body-content').first().html() ?? '';
+ const audioSrc = $('#player-default').attr('src');
+ if (audioSrc) {
+ description = `
${description}`;
+ }
+ return {
+ title: linkData?.title ?? $('head title').text(),
+ pubDate: linkData?.pubDate,
+ description,
+ enclosure_url: audioSrc,
+ enclosure_type: 'audio/mpeg',
+ };
+}
+
+function parseTaskForceReport($: CheerioAPI): DataItem {
+ const linkData = parseLinkData($);
+
+ let description = '';
+
+ $('.main-content').each((_, ele) => {
+ const $ele = $(ele);
+ const content = $ele.find('.content_area').html() ?? '';
+ description += `${content}
`;
+ });
+
+ return {
+ title: linkData?.title ?? $('.hero__title').remove('.subtitle').text(),
+ pubDate: linkData?.pubDate,
+ description,
+ };
+}
+
+function parseTimeline($: CheerioAPI): DataItem {
+ const linkData = parseLinkData($);
+
+ const $description = $('.timeline-slides');
+ $description.find('.timeline-slide__shadow').remove();
+ $description.find('.field--image').each((_, ele) => {
+ $(ele).replaceWith($(ele).find('img'));
+ });
+ let description = $description.find('.timeline-intro__description').html() ?? '';
+ for (const item of $description.find('.timeline-slide__content').toArray()) {
+ const $item = $(item);
+ $item.find('.timeline-slide__dates-header').replaceWith('' + $item.find('.timeline-slide__dates-header').text() + '
');
+ $item.find('.timeline-slide__dates').replaceWith('' + $item.find('.timeline-slide__dates').text() + '
');
+ description += `
${$item.html()}`;
+ }
+ return {
+ title: linkData?.title ?? $('.timeline-header__title').text(),
+ pubDate: linkData?.pubDate,
+ description,
+ };
+}
+
+function parseVideo($: CheerioAPI): DataItem {
+ const linkData = parseLinkData($);
+ let description = parseDescription($('.body-content'), $);
+ const $articleHeader = $('.article-header__image');
+ const videoIfame = getVideoIframe($articleHeader);
+ if (videoIfame) {
+ description = `${videoIfame}
${description}`;
+ }
+
+ return {
+ title: linkData?.title ?? $('.article-header__title').text(),
+ pubDate: linkData?.pubDate,
+ description,
+ };
+}
+
+function parseDefault($): DataItem {
+ if ($('.body-content').length) {
+ return parseArticle($);
+ }
+ const linkData = parseLinkData($);
+ return {
+ title: linkData?.title ?? $('head title').text(),
+ pubDate: linkData?.pubDate,
+ };
+}
+
+function parseLinkData($: CheerioAPI) {
+ try {
+ const data = (更多分类
+
+#### [党建园地](https://www.chinacdc.cn/dqgz/djgz/)
+
+| [党建工作](https://www.chinacdc.cn/dqgz/djgz/) | [廉政文化](https://www.chinacdc.cn/djgz_13611/) | [工会工作](https://www.chinacdc.cn/ghgz/) | [团青工作](https://www.chinacdc.cn/tqgz/) | [理论学习](https://www.chinacdc.cn/tqgz_13618/) |
+| -------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------------- |
+| [dqgz/djgz](https://rsshub.app/chinacdc/dqgz/djgz) | [dqgz/djgz_13611](https://rsshub.app/chinacdc/dqgz/djgz_13611) | [dqgz/ghgz](https://rsshub.app/chinacdc/dqgz/ghgz) | [dqgz/tqgz](https://rsshub.app/chinacdc/dqgz/tqgz) | [dqgz/tqgz_13618](https://rsshub.app/chinacdc/dqgz/tqgz_13618) |
+
+#### [疾控应急](https://www.chinacdc.cn/jkyj/)
+
+| [传染病](https://www.chinacdc.cn/jkyj/crb2/) | [突发公共卫生事件](https://www.chinacdc.cn/jkyj/tfggws/) | [慢性病与伤害防控](https://www.chinacdc.cn/jkyj/mxfcrxjb2/) | [烟草控制](https://www.chinacdc.cn/jkyj/yckz/) | [营养与健康](https://www.chinacdc.cn/jkyj/yyyjk2/) |
+| -------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------- | ------------------------------------------------------ |
+| [jkyj/crb2](https://rsshub.app/chinacdc/jkyj/crb2) | [jkyj/tfggws](https://rsshub.app/chinacdc/jkyj/tfggws) | [jkyj/mxfcrxjb2](https://rsshub.app/chinacdc/jkyj/mxfcrxjb2) | [jkyj/yckz](https://rsshub.app/chinacdc/jkyj/yckz) | [jkyj/yyyjk2](https://rsshub.app/chinacdc/jkyj/yyyjk2) |
+
+| [环境与健康](https://www.chinacdc.cn/jkyj/hjyjk/) | [职业卫生与中毒控制](https://www.chinacdc.cn/jkyj/hjwsyzdkz/) | [放射卫生](https://www.chinacdc.cn/jkyj/fsws/) | [免疫规划](https://www.chinacdc.cn/jkyj/mygh02/) | [结核病防控](https://www.chinacdc.cn/jkyj/jhbfk/) |
+| ---------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------- |
+| [jkyj/hjyjk](https://rsshub.app/chinacdc/jkyj/hjyjk) | [jkyj/hjwsyzdkz](https://rsshub.app/chinacdc/jkyj/hjwsyzdkz) | [jkyj/fsws](https://rsshub.app/chinacdc/jkyj/fsws) | [jkyj/mygh02](https://rsshub.app/chinacdc/jkyj/mygh02) | [jkyj/jhbfk](https://rsshub.app/chinacdc/jkyj/jhbfk) |
+
+| [寄生虫病](https://www.chinacdc.cn/jkyj/jscb/) |
+| -------------------------------------------------- |
+| [jkyj/jscb](https://rsshub.app/chinacdc/jkyj/jscb) |
+
+#### [科学研究](https://www.chinacdc.cn/kxyj/)
+
+| [科技进展](https://www.chinacdc.cn/kxyj/kjjz/) | [学术动态](https://www.chinacdc.cn/kxyj/xsdt/) | [科研平台](https://www.chinacdc.cn/kxyj/xsjl/) | [科研亮点](https://www.chinacdc.cn/kxyj/kyld/) | [科技政策](https://www.chinacdc.cn/kxyj/kjzc/) |
+| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
+| [kxyj/kjjz](https://rsshub.app/chinacdc/kxyj/kjjz) | [kxyj/xsdt](https://rsshub.app/chinacdc/kxyj/xsdt) | [kxyj/xsjl](https://rsshub.app/chinacdc/kxyj/xsjl) | [kxyj/kyld](https://rsshub.app/chinacdc/kxyj/kyld) | [kxyj/kjzc](https://rsshub.app/chinacdc/kxyj/kjzc) |
+
+#### [教育培训](https://www.chinacdc.cn/jypx/)
+
+| [研究生院](https://www.chinacdc.cn/jypx/yjsy/) | [继续教育](https://www.chinacdc.cn/jypx/jxjy/) | [博士后](https://www.chinacdc.cn/jypx/bsh/) | [中国现场流行病学培训项目(CFETP)](https://www.chinacdc.cn/jypx/CFETP/) |
+| -------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------ |
+| [jypx/yjsy](https://rsshub.app/chinacdc/jypx/yjsy) | [jypx/jxjy](https://rsshub.app/chinacdc/jypx/jxjy) | [jypx/bsh](https://rsshub.app/chinacdc/jypx/bsh) | [jypx/CFETP](https://rsshub.app/chinacdc/jypx/CFETP) |
+
+#### [全球公卫](https://www.chinacdc.cn/qqgw/)
+
+| [合作伙伴](https://www.chinacdc.cn/qqgw/hzhb/) | [世界卫生组织合作中心和参比实验室](https://www.chinacdc.cn/qqgw/wszz/) | [国际交流 (港澳台交流)](https://www.chinacdc.cn/qqgw/gjjl/) | [公共卫生援外与合作](https://www.chinacdc.cn/qqgw/ggws/) |
+| -------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------- |
+| [qqgw/hzhb](https://rsshub.app/chinacdc/qqgw/hzhb) | [qqgw/wszz](https://rsshub.app/chinacdc/qqgw/wszz) | [qqgw/gjjl](https://rsshub.app/chinacdc/qqgw/gjjl) | [qqgw/ggws](https://rsshub.app/chinacdc/qqgw/ggws) |
+
+#### [人才建设](https://www.chinacdc.cn/rcjs/)
+
+| [院士风采](https://www.chinacdc.cn/rcjs/ysfc/) | [首席专家](https://www.chinacdc.cn/rcjs/sxzj/) | [人才队伍](https://www.chinacdc.cn/rcjs/rcdw/) | [人才招聘](https://www.chinacdc.cn/rcjs/rczp/) |
+| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
+| [rcjs/ysfc](https://rsshub.app/chinacdc/rcjs/ysfc) | [rcjs/sxzj](https://rsshub.app/chinacdc/rcjs/sxzj) | [rcjs/rcdw](https://rsshub.app/chinacdc/rcjs/rcdw) | [rcjs/rczp](https://rsshub.app/chinacdc/rcjs/rczp) |
+
+#### [健康数据](https://www.chinacdc.cn/jksj/)
+
+| [全国法定传染病疫情情况](https://www.chinacdc.cn/jksj/jksj01/) | [全国新型冠状病毒感染疫情情况](https://www.chinacdc.cn/jksj/xgbdyq/) | [重点传染病和突发公共卫生事件风险评估报告](https://www.chinacdc.cn/jksj/jksj02/) | [全球传染病事件风险评估报告](https://www.chinacdc.cn/jksj/jksj03/) | [全国预防接种异常反应监测信息概况](https://www.chinacdc.cn/jksj/jksj04_14209/) |
+| -------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
+| [jksj/jksj01](https://rsshub.app/chinacdc/jksj/jksj01) | [jksj/xgbdyq](https://rsshub.app/chinacdc/jksj/xgbdyq) | [jksj/jksj02](https://rsshub.app/chinacdc/jksj/jksj02) | [jksj/jksj03](https://rsshub.app/chinacdc/jksj/jksj03) | [jksj/jksj04_14209](https://rsshub.app/chinacdc/jksj/jksj04_14209) |
+
+| [流感监测周报](https://www.chinacdc.cn/jksj/jksj04_14249/) | [全国急性呼吸道传染病哨点监测情况](https://www.chinacdc.cn/jksj/jksj04_14275/) | [健康报告](https://www.chinacdc.cn/jksj/jksj04/) |
+| ------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------ |
+| [jksj/jksj04_14249](https://rsshub.app/chinacdc/jksj/jksj04_14249) | [jksj/jksj04_14275](https://rsshub.app/chinacdc/jksj/jksj04_14275) | [jksj/jksj04](https://rsshub.app/chinacdc/jksj/jksj04) |
+
+#### [健康科普](https://www.chinacdc.cn/jkkp/)
+
+| [传染病](https://www.chinacdc.cn/jkkp/crb/) | [慢性非传染性疾病](https://www.chinacdc.cn/jkkp/mxfcrb/) | [免疫规划](https://www.chinacdc.cn/jkkp/mygh/) | [公共卫生事件](https://www.chinacdc.cn/jkkp/ggws/) | [烟草控制](https://www.chinacdc.cn/jkkp/yckz/) |
+| ------------------------------------------------ | -------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
+| [jkkp/crb](https://rsshub.app/chinacdc/jkkp/crb) | [jkkp/mxfcrb](https://rsshub.app/chinacdc/jkkp/mxfcrb) | [jkkp/mygh](https://rsshub.app/chinacdc/jkkp/mygh) | [jkkp/ggws](https://rsshub.app/chinacdc/jkkp/ggws) | [jkkp/yckz](https://rsshub.app/chinacdc/jkkp/yckz) |
+
+| [营养与健康](https://www.chinacdc.cn/jkkp/yyjk/) | [环境健康](https://www.chinacdc.cn/jkkp/hjjk/) | [职业健康与中毒控制](https://www.chinacdc.cn/jkkp/zyjk/) | [放射卫生](https://www.chinacdc.cn/jkkp/fsws/) |
+| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------- |
+| [jkkp/yyjk](https://rsshub.app/chinacdc/jkkp/yyjk) | [jkkp/hjjk](https://rsshub.app/chinacdc/jkkp/hjjk) | [jkkp/zyjk](https://rsshub.app/chinacdc/jkkp/zyjk) | [jkkp/fsws](https://rsshub.app/chinacdc/jkkp/fsws) |
+
+{{ intro }}
+{{ /if }}
+
+{{ if description }}
+ {{@ description }}
+{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/chinadegrees/namespace.ts b/lib/routes/chinadegrees/namespace.ts
index 7773eec5214541..e6f755a7b73103 100644
--- a/lib/routes/chinadegrees/namespace.ts
+++ b/lib/routes/chinadegrees/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中华人民共和国学位证书查询',
url: 'chinadegrees.com.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/chinadegrees/province.ts b/lib/routes/chinadegrees/province.ts
index f37fb760d3024b..df6ca73efca482 100644
--- a/lib/routes/chinadegrees/province.ts
+++ b/lib/routes/chinadegrees/province.ts
@@ -26,39 +26,39 @@ export const route: Route = {
},
name: '各学位授予单位学位证书上网进度',
description: `| 省市 | 代号 |
- | ---------------- | ---- |
- | 北京市 | 11 |
- | 天津市 | 12 |
- | 河北省 | 13 |
- | 山西省 | 14 |
- | 内蒙古自治区 | 15 |
- | 辽宁省 | 21 |
- | 吉林省 | 22 |
- | 黑龙江省 | 23 |
- | 上海市 | 31 |
- | 江苏省 | 32 |
- | 浙江省 | 33 |
- | 安徽省 | 34 |
- | 福建省 | 35 |
- | 江西省 | 36 |
- | 山东省 | 37 |
- | 河南省 | 41 |
- | 湖北省 | 42 |
- | 湖南省 | 43 |
- | 广东省 | 44 |
- | 广西壮族自治区 | 45 |
- | 海南省 | 46 |
- | 重庆市 | 50 |
- | 四川省 | 51 |
- | 贵州省 | 52 |
- | 云南省 | 53 |
- | 西藏自治区 | 54 |
- | 陕西省 | 61 |
- | 甘肃省 | 62 |
- | 青海省 | 63 |
- | 宁夏回族自治区 | 64 |
- | 新疆维吾尔自治区 | 65 |
- | 台湾 | 71 |`,
+| ---------------- | ---- |
+| 北京市 | 11 |
+| 天津市 | 12 |
+| 河北省 | 13 |
+| 山西省 | 14 |
+| 内蒙古自治区 | 15 |
+| 辽宁省 | 21 |
+| 吉林省 | 22 |
+| 黑龙江省 | 23 |
+| 上海市 | 31 |
+| 江苏省 | 32 |
+| 浙江省 | 33 |
+| 安徽省 | 34 |
+| 福建省 | 35 |
+| 江西省 | 36 |
+| 山东省 | 37 |
+| 河南省 | 41 |
+| 湖北省 | 42 |
+| 湖南省 | 43 |
+| 广东省 | 44 |
+| 广西壮族自治区 | 45 |
+| 海南省 | 46 |
+| 重庆市 | 50 |
+| 四川省 | 51 |
+| 贵州省 | 52 |
+| 云南省 | 53 |
+| 西藏自治区 | 54 |
+| 陕西省 | 61 |
+| 甘肃省 | 62 |
+| 青海省 | 63 |
+| 宁夏回族自治区 | 64 |
+| 新疆维吾尔自治区 | 65 |
+| 台湾 | 71 |`,
maintainers: ['TonyRL'],
handler,
};
diff --git a/lib/routes/chinafactcheck/namespace.ts b/lib/routes/chinafactcheck/namespace.ts
index ccd631dad68f33..94165553f96908 100644
--- a/lib/routes/chinafactcheck/namespace.ts
+++ b/lib/routes/chinafactcheck/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '有据',
url: 'chinafactcheck.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/chinaisa/index.ts b/lib/routes/chinaisa/index.ts
index 0e4f71fb387f34..17bc02430d8d33 100644
--- a/lib/routes/chinaisa/index.ts
+++ b/lib/routes/chinaisa/index.ts
@@ -1,6 +1,6 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
-import got from '@/utils/got';
+import ofetch from '@/utils/ofetch';
import { load } from 'cheerio';
import { parseDate } from '@/utils/parse-date';
@@ -20,144 +20,144 @@ export const route: Route = {
name: '栏目',
maintainers: ['nczitzk'],
handler,
- description: `| 栏目 | id |
- | -------- | ---------------------------------------------------------------- |
- | 钢协动态 | 58af05dfb6b4300151760176d2aad0a04c275aaadbb1315039263f021f920dcd |
- | 钢协要闻 | 67ea4f106bd8f0843c0538d43833c463a0cd411fc35642cbd555a5f39fcf352b |
- | 会议报道 | e5070694f299a43b20d990e53b6a69dc02e755fef644ae667cf75deaff80407a |
- | 领导讲话 | a873c2e67b26b4a2d8313da769f6e106abc9a1ff04b7f1a50674dfa47cf91a7b |
- | 图片新闻 | 806254321b2459bddb3c2cb5590fef6332bd849079d3082daf6153d7f8d62e1e |
-
- 更多栏目
-
- #### 党建工作
-
- | 栏目 | id |
- | ---------------------------------------------------- | ---------------------------------------------------------------- |
- | 党建工作 | 10e8911e0c852d91f08e173c768700da608abfb4e7b0540cb49fa5498f33522b |
- | 学习贯彻习近平新时代中国特色社会主义思想主题教育专栏 | b7a7ad4b5d8ffaca4b29f3538fd289da9d07f827f89e6ea57ef07257498aacf9 |
- | 党史学习教育专栏 | 4d8e7dec1b672704916331431156ea7628a598c191d751e4fc28408ccbd4e0c4 |
- | 不忘初心、牢记使命 | 427f7c28c90ec9db1aab78db8156a63ff2e23f6a0cea693e3847fe6d595753db |
- | 两学一做 | 5b0609fedc9052bb44f1cfe9acf5ec8c9fe960f22a07be69636f2cf1cacaa8f7 |
- | 钢协党代会 | beaaa0314f0f532d4b18244cd70df614a4af97465d974401b1f5b3349d78144b |
- | 创先争优 | e7ea82c886ba18691210aaf48b3582a92dca9c4f2aab912757cedafb066ff8a6 |
- | 青年工作 | 2706ee3a4a4c3c23e90e13c8fdc3002855d1dba394b61626562a97b33af3dbd0 |
- | 日常动态 | e21157a082fc0ab0d7062c8755e91472ee0d23de6ccc5c2a44b62e54062cf1e4 |
-
- #### 要闻
-
- | 栏目 | id |
- | ------------ | ---------------------------------------------------------------- |
- | 要闻 | c42511ce3f868a515b49668dd250290c80d4dc8930c7e455d0e6e14b8033eae2 |
- | 会员动态 | 268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c |
- | 疫情应对专栏 | a83c48faeb34065fd9b33d3c84957a152675141458aedc0ec454b760c9fcad65 |
-
- #### 统计发布
-
- | 栏目 | id |
- | -------- | ---------------------------------------------------------------- |
- | 统计发布 | 2e3c87064bdfc0e43d542d87fce8bcbc8fe0463d5a3da04d7e11b4c7d692194b |
- | 生产经营 | 3238889ba0fa3aabcf28f40e537d440916a361c9170a4054f9fc43517cb58c1e |
- | 进出口 | 95ef75c752af3b6c8be479479d8b931de7418c00150720280d78c8f0da0a438c |
- | 环保统计 | 619ce7b53a4291d47c19d0ee0765098ca435e252576fbe921280a63fba4bc712 |
-
- #### 行业分析
-
- | 栏目 | id |
- | -------- | ---------------------------------------------------------------- |
- | 行业分析 | 1b4316d9238e09c735365896c8e4f677a3234e8363e5622ae6e79a5900a76f56 |
- | 市场分析 | a44207e193a5caa5e64102604b6933896a0025eb85c57c583b39626f33d4dafd |
- | 板带材 | 05d0e136828584d2cd6e45bdc3270372764781b98546cce122d9974489b1e2f2 |
- | 社会库存 | 197422a82d9a09b9cc86188444574816e93186f2fde87474f8b028fc61472d35 |
-
- #### 钢材价格指数
-
- | 栏目 | id |
- | ------------ | ---------------------------------------------------------------- |
- | 钢材价格指数 | 17b6a9a214c94ccc28e56d4d1a2dbb5acef3e73da431ddc0a849a4dcfc487d04 |
- | 综合价格指数 | 63913b906a7a663f7f71961952b1ddfa845714b5982655b773a62b85dd3b064e |
- | 地区价格 | fc816c75aed82b9bc25563edc9cf0a0488a2012da38cbef5258da614d6e51ba9 |
-
- #### 宏观经济信息
-
- | 栏目 | id |
- | ------------ | ---------------------------------------------------------------- |
- | 宏观经济信息 | 5d77b433182404193834120ceed16fe0625860fafd5fd9e71d0800c4df227060 |
- | 相关行业信息 | ae2a3c0fd4936acf75f4aab6fadd08bc6371aa65bdd50419e74b70d6f043c473 |
- | 国际动态 | 1bad7c56af746a666e4a4e56e54a9508d344d7bc1498360580613590c16b6c41 |
-
- #### 专题报道
-
- | 栏目 | id |
- | -------------------- | ---------------------------------------------------------------- |
- | 专题报道 | 50e7242bfd78b4395f3338df7699a0ff8847b886c4c3a55bd7c102a2cfe32fe9 |
- | 钢协理事会 | 40c6404418699f0f8cb4e513013bb110ef250c782f0959852601e7c75e1afcd8 |
- | 钢协新闻发布会 | 11ea370f565c6c141b1a4dac60aa00c4331bd442382a5dd476a5e73e001b773c |
- | 劳模表彰 | 907e4ae217bf9c981a132051572103f9c87cccb7f00caf5a1770078829e6bcb3 |
- | 钢铁行业职业技能竞赛 | 563c15270a691e3c7cb9cd9ba457c5af392eb4630fa833fc1a55c8e2afbc28a9 |
-
- #### 成果奖励
-
- | 栏目 | id |
- | ---------------------- | ---------------------------------------------------------------- |
- | 成果奖励 | a6c30053b66356b4d77fbf6668bda69f7e782b2ae08a21d5db171d50a504bd40 |
- | 冶金科学技术奖 | 50fe0c63f657ee48e49cb13fe7f7c5502046acdb05e2ee8a317f907af4191683 |
- | 企业管理现代化创新成果 | b5607d3b73c2c3a3b069a97b9dbfd59af64aea27bafd5eb87ba44d1b07a33b66 |
- | 清洁生产环境友好企业 | 4475c8e21374d063a22f95939a2909837e78fab1832dc97bf64f09fa01c0c5f7 |
- | 产品开发市场开拓奖 | 169e34d7b29e3deaf4d4496da594d3bbde2eb0a40f7244b54dbfb9cc89a37296 |
- | 质量金杯奖 | 68029784be6d9a7bf9cb8cace5b8a5ce5d2d871e9a0cbcbf84eeae0ea2746311 |
-
- #### 节能减排
-
- | 栏目 | id |
- | ------------------------------------------ | ---------------------------------------------------------------- |
- | 节能减排 | 08895f1681c198fdf297ab38e33e1f428f6ccf2add382f3844a52e410f10e5a0 |
- | 先进节能环保技术 | 6e639343a517fd08e5860fba581d41940da523753956ada973b6952fc05ef94f |
- | 钢铁企业超低排放改造和评估监测进展情况公示 | 50d99531d5dee68346653ca9548f308764ad38410a091e662834a5ed66770174 |
-
- #### 国际交流
-
- | 栏目 | id |
- | -------- | ---------------------------------------------------------------- |
- | 国际交流 | 4753eef81b4019369d4751413d852ab9027944b84c612b5a08614e046d169e81 |
- | 外事动态 | aa590ec6f835136a9ce8c9f3d0c3b194beb6b78037466ab40bb4aacc32adfcc9 |
- | 国际会展 | 05ac1f2971bc375d25c9112e399f9c3cbb237809684ebc5b0ca4a68a1fcb971c |
-
- #### 政策法规
-
- | 栏目 | id |
- | -------- | ---------------------------------------------------------------- |
- | 政策法规 | 63a69eb0087f1984c0b269a1541905f19a56e117d56b3f51dfae0e6c1d436533 |
- | 政策法规 | a214b2e71c3c79fa4a36ff382ee5f822b9603634626f7e320f91ed696b3666f2 |
- | 贸易规则 | 5988b2380d04d3efde8cc247377d19530c17904ec0b5decdd00f9b3e026e3715 |
-
- #### 分会园地
-
- | 栏目 | id |
- | ------------ | ---------------------------------------------------------------- |
- | 分会园地 | d059d6751dcaae94e31a795072267f7959c35d012eebb9858b3ede2990e82ea9 |
- | 法律分会 | 96000647f18ea78fa134a3932563e7d27c68d0482de498f179b44846234567a9 |
- | 设备分会 | c8e1e3f52406115c2c03928271bbe883c0875b7c9f2f67492395685a62a1a2d8 |
- | 国际产能合作 | 4fb8cc4b0d6f905a969ac3375f6d17b34df4dcae69d798d2a4616daa80af020c |
- | 绿化分会 | ad55a0fbc1a44e94fb60e21b98cf967aca17ecf1450bdfb3699468fe8235103b |
-
- #### 钢铁知识
-
- | 栏目 | id |
- | ------------ | ---------------------------------------------------------------- |
- | 钢铁知识 | 7f7509ff045023015e0d6c1ba22c32734b673be2ec14eae730a99c08e3badb3f |
- | 钢铁材料使用 | 7e319d71258ed6bb663cf59b4cf67fe97894e60aa5520f3d2cf966f82f9b89ac |
- | 钢铁标准 | fae0c4dd27f8fe4759941e78c9dc1dfe0088ce30d1b684d12be4c8172d2c08e1 |
-
- #### 钢协刊物
-
- | 栏目 | id |
- | ---------- | ---------------------------------------------------------------- |
- | 钢协刊物 | ed51af486f6d4b313b3aaf8fea0b32a4a2d4a89714c61992caf01942eb61831b |
- | 中国钢铁业 | 6440bdfccadf87908b13d8bbd9a66bb89bbd60cc5e175c018ca1c62c7d55e61f |
- | 钢铁信息 | 2b66af0b2cda9b420739e55e255a6f72f277557670ef861c9956da8fde25da05 |
- 更多栏目
+
+#### 党建工作
+
+| 栏目 | id |
+| ---------------------------------------------------- | ---------------------------------------------------------------- |
+| 党建工作 | 10e8911e0c852d91f08e173c768700da608abfb4e7b0540cb49fa5498f33522b |
+| 学习贯彻习近平新时代中国特色社会主义思想主题教育专栏 | b7a7ad4b5d8ffaca4b29f3538fd289da9d07f827f89e6ea57ef07257498aacf9 |
+| 党史学习教育专栏 | 4d8e7dec1b672704916331431156ea7628a598c191d751e4fc28408ccbd4e0c4 |
+| 不忘初心、牢记使命 | 427f7c28c90ec9db1aab78db8156a63ff2e23f6a0cea693e3847fe6d595753db |
+| 两学一做 | 5b0609fedc9052bb44f1cfe9acf5ec8c9fe960f22a07be69636f2cf1cacaa8f7 |
+| 钢协党代会 | beaaa0314f0f532d4b18244cd70df614a4af97465d974401b1f5b3349d78144b |
+| 创先争优 | e7ea82c886ba18691210aaf48b3582a92dca9c4f2aab912757cedafb066ff8a6 |
+| 青年工作 | 2706ee3a4a4c3c23e90e13c8fdc3002855d1dba394b61626562a97b33af3dbd0 |
+| 日常动态 | e21157a082fc0ab0d7062c8755e91472ee0d23de6ccc5c2a44b62e54062cf1e4 |
+
+#### 要闻
+
+| 栏目 | id |
+| ------------ | ---------------------------------------------------------------- |
+| 要闻 | c42511ce3f868a515b49668dd250290c80d4dc8930c7e455d0e6e14b8033eae2 |
+| 会员动态 | 268f86fdf61ac8614f09db38a2d0295253043b03e092c7ff48ab94290296125c |
+| 疫情应对专栏 | a83c48faeb34065fd9b33d3c84957a152675141458aedc0ec454b760c9fcad65 |
+
+#### 统计发布
+
+| 栏目 | id |
+| -------- | ---------------------------------------------------------------- |
+| 统计发布 | 2e3c87064bdfc0e43d542d87fce8bcbc8fe0463d5a3da04d7e11b4c7d692194b |
+| 生产经营 | 3238889ba0fa3aabcf28f40e537d440916a361c9170a4054f9fc43517cb58c1e |
+| 进出口 | 95ef75c752af3b6c8be479479d8b931de7418c00150720280d78c8f0da0a438c |
+| 环保统计 | 619ce7b53a4291d47c19d0ee0765098ca435e252576fbe921280a63fba4bc712 |
+
+#### 行业分析
+
+| 栏目 | id |
+| -------- | ---------------------------------------------------------------- |
+| 行业分析 | 1b4316d9238e09c735365896c8e4f677a3234e8363e5622ae6e79a5900a76f56 |
+| 市场分析 | a44207e193a5caa5e64102604b6933896a0025eb85c57c583b39626f33d4dafd |
+| 板带材 | 05d0e136828584d2cd6e45bdc3270372764781b98546cce122d9974489b1e2f2 |
+| 社会库存 | 197422a82d9a09b9cc86188444574816e93186f2fde87474f8b028fc61472d35 |
+
+#### 钢材价格指数
+
+| 栏目 | id |
+| ------------ | ---------------------------------------------------------------- |
+| 钢材价格指数 | 17b6a9a214c94ccc28e56d4d1a2dbb5acef3e73da431ddc0a849a4dcfc487d04 |
+| 综合价格指数 | 63913b906a7a663f7f71961952b1ddfa845714b5982655b773a62b85dd3b064e |
+| 地区价格 | fc816c75aed82b9bc25563edc9cf0a0488a2012da38cbef5258da614d6e51ba9 |
+
+#### 宏观经济信息
+
+| 栏目 | id |
+| ------------ | ---------------------------------------------------------------- |
+| 宏观经济信息 | 5d77b433182404193834120ceed16fe0625860fafd5fd9e71d0800c4df227060 |
+| 相关行业信息 | ae2a3c0fd4936acf75f4aab6fadd08bc6371aa65bdd50419e74b70d6f043c473 |
+| 国际动态 | 1bad7c56af746a666e4a4e56e54a9508d344d7bc1498360580613590c16b6c41 |
+
+#### 专题报道
+
+| 栏目 | id |
+| -------------------- | ---------------------------------------------------------------- |
+| 专题报道 | 50e7242bfd78b4395f3338df7699a0ff8847b886c4c3a55bd7c102a2cfe32fe9 |
+| 钢协理事会 | 40c6404418699f0f8cb4e513013bb110ef250c782f0959852601e7c75e1afcd8 |
+| 钢协新闻发布会 | 11ea370f565c6c141b1a4dac60aa00c4331bd442382a5dd476a5e73e001b773c |
+| 劳模表彰 | 907e4ae217bf9c981a132051572103f9c87cccb7f00caf5a1770078829e6bcb3 |
+| 钢铁行业职业技能竞赛 | 563c15270a691e3c7cb9cd9ba457c5af392eb4630fa833fc1a55c8e2afbc28a9 |
+
+#### 成果奖励
+
+| 栏目 | id |
+| ---------------------- | ---------------------------------------------------------------- |
+| 成果奖励 | a6c30053b66356b4d77fbf6668bda69f7e782b2ae08a21d5db171d50a504bd40 |
+| 冶金科学技术奖 | 50fe0c63f657ee48e49cb13fe7f7c5502046acdb05e2ee8a317f907af4191683 |
+| 企业管理现代化创新成果 | b5607d3b73c2c3a3b069a97b9dbfd59af64aea27bafd5eb87ba44d1b07a33b66 |
+| 清洁生产环境友好企业 | 4475c8e21374d063a22f95939a2909837e78fab1832dc97bf64f09fa01c0c5f7 |
+| 产品开发市场开拓奖 | 169e34d7b29e3deaf4d4496da594d3bbde2eb0a40f7244b54dbfb9cc89a37296 |
+| 质量金杯奖 | 68029784be6d9a7bf9cb8cace5b8a5ce5d2d871e9a0cbcbf84eeae0ea2746311 |
+
+#### 节能减排
+
+| 栏目 | id |
+| ------------------------------------------ | ---------------------------------------------------------------- |
+| 节能减排 | 08895f1681c198fdf297ab38e33e1f428f6ccf2add382f3844a52e410f10e5a0 |
+| 先进节能环保技术 | 6e639343a517fd08e5860fba581d41940da523753956ada973b6952fc05ef94f |
+| 钢铁企业超低排放改造和评估监测进展情况公示 | 50d99531d5dee68346653ca9548f308764ad38410a091e662834a5ed66770174 |
+
+#### 国际交流
+
+| 栏目 | id |
+| -------- | ---------------------------------------------------------------- |
+| 国际交流 | 4753eef81b4019369d4751413d852ab9027944b84c612b5a08614e046d169e81 |
+| 外事动态 | aa590ec6f835136a9ce8c9f3d0c3b194beb6b78037466ab40bb4aacc32adfcc9 |
+| 国际会展 | 05ac1f2971bc375d25c9112e399f9c3cbb237809684ebc5b0ca4a68a1fcb971c |
+
+#### 政策法规
+
+| 栏目 | id |
+| -------- | ---------------------------------------------------------------- |
+| 政策法规 | 63a69eb0087f1984c0b269a1541905f19a56e117d56b3f51dfae0e6c1d436533 |
+| 政策法规 | a214b2e71c3c79fa4a36ff382ee5f822b9603634626f7e320f91ed696b3666f2 |
+| 贸易规则 | 5988b2380d04d3efde8cc247377d19530c17904ec0b5decdd00f9b3e026e3715 |
+
+#### 分会园地
+
+| 栏目 | id |
+| ------------ | ---------------------------------------------------------------- |
+| 分会园地 | d059d6751dcaae94e31a795072267f7959c35d012eebb9858b3ede2990e82ea9 |
+| 法律分会 | 96000647f18ea78fa134a3932563e7d27c68d0482de498f179b44846234567a9 |
+| 设备分会 | c8e1e3f52406115c2c03928271bbe883c0875b7c9f2f67492395685a62a1a2d8 |
+| 国际产能合作 | 4fb8cc4b0d6f905a969ac3375f6d17b34df4dcae69d798d2a4616daa80af020c |
+| 绿化分会 | ad55a0fbc1a44e94fb60e21b98cf967aca17ecf1450bdfb3699468fe8235103b |
+
+#### 钢铁知识
+
+| 栏目 | id |
+| ------------ | ---------------------------------------------------------------- |
+| 钢铁知识 | 7f7509ff045023015e0d6c1ba22c32734b673be2ec14eae730a99c08e3badb3f |
+| 钢铁材料使用 | 7e319d71258ed6bb663cf59b4cf67fe97894e60aa5520f3d2cf966f82f9b89ac |
+| 钢铁标准 | fae0c4dd27f8fe4759941e78c9dc1dfe0088ce30d1b684d12be4c8172d2c08e1 |
+
+#### 钢协刊物
+
+| 栏目 | id |
+| ---------- | ---------------------------------------------------------------- |
+| 钢协刊物 | ed51af486f6d4b313b3aaf8fea0b32a4a2d4a89714c61992caf01942eb61831b |
+| 中国钢铁业 | 6440bdfccadf87908b13d8bbd9a66bb89bbd60cc5e175c018ca1c62c7d55e61f |
+| 钢铁信息 | 2b66af0b2cda9b420739e55e255a6f72f277557670ef861c9956da8fde25da05 |
+市场公告
+市场公告
外汇市场公告
- | 最新 | 市场公告通知 | 中心会员公告 | 会员信息公告 |
- | ---- | ------------ | ------------ | ------------ |
- | 2834 | 2835 | 2836 | 2837 |
+| 最新 | 市场公告通知 | 中心会员公告 | 会员信息公告 |
+| ---- | ------------ | ------------ | ------------ |
+| 2834 | 2835 | 2836 | 2837 |
本币市场公告
- | 最新 | 市场公告通知 | 中心会员公告 | 会员信息公告 |
- | -------------- | ------------ | ------------ | ------------ |
- | 2839,2840,2841 | 2839 | 2840 | 2841 |
+| 最新 | 市场公告通知 | 中心会员公告 | 会员信息公告 |
+| -------------- | ------------ | ------------ | ------------ |
+| 2839,2840,2841 | 2839 | 2840 | 2841 |
央行业务公告
- | 最新 | 公开市场操作 | 中央国库现金管理 |
- | --------- | ------------ | ---------------- |
- | 2845,2846 | 2845 | 2846 |
- 本币市场
+本币市场
贷款市场报价利率
- | LPR 市场公告 |
- | ------------ |
- | 3686 |
- 更多分类
+
+#### [协会动态](https://www.chinania.org.cn/html/xiehuidongtai/)
+
+| [协会动态](https://www.chinania.org.cn/html/xiehuidongtai/xiehuidongtai/) | [协会通知](https://www.chinania.org.cn/html/xiehuidongtai/xiehuitongzhi/) | [有色企业50强](https://www.chinania.org.cn/html/xiehuidongtai/youseqiye50qiang/) |
+| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
+| [xiehuidongtai/xiehuidongtai](https://rsshub.app/chinania/xiehuidongtai/xiehuidongtai) | [xiehuidongtai/xiehuitongzhi](https://rsshub.app/chinania/xiehuidongtai/xiehuitongzhi) | [xiehuidongtai/youseqiye50qiang](https://rsshub.app/chinania/xiehuidongtai/youseqiye50qiang) |
+
+#### [党建工作](https://www.chinania.org.cn/html/djgz/)
+
+| [协会党建](https://www.chinania.org.cn/html/djgz/xiehuidangjian/) | [行业党建](https://www.chinania.org.cn/html/djgz/hangyedangjian/) |
+| ---------------------------------------------------------------------- | ---------------------------------------------------------------------- |
+| [djgz/xiehuidangjian](https://rsshub.app/chinania/djgz/xiehuidangjian) | [djgz/hangyedangjian](https://rsshub.app/chinania/djgz/hangyedangjian) |
+
+#### [行业新闻](https://www.chinania.org.cn/html/hangyexinwen/)
+
+| [时政要闻](https://www.chinania.org.cn/html/hangyexinwen/shizhengyaowen/) | [要闻](https://www.chinania.org.cn/html/hangyexinwen/yaowen/) | [行业新闻](https://www.chinania.org.cn/html/hangyexinwen/guoneixinwen/) | [资讯](https://www.chinania.org.cn/html/hangyexinwen/zixun/) |
+| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
+| [hangyexinwen/shizhengyaowen](https://rsshub.app/chinania/hangyexinwen/shizhengyaowen) | [hangyexinwen/yaowen](https://rsshub.app/chinania/hangyexinwen/yaowen) | [hangyexinwen/guoneixinwen](https://rsshub.app/chinania/hangyexinwen/guoneixinwen) | [hangyexinwen/zixun](https://rsshub.app/chinania/hangyexinwen/zixun) |
+
+#### [人力资源](https://www.chinania.org.cn/html/renliziyuan/)
+
+| [相关通知](https://www.chinania.org.cn/html/renliziyuan/xiangguantongzhi/) | [人事招聘](https://www.chinania.org.cn/html/renliziyuan/renshizhaopin/) |
+| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
+| [renliziyuan/xiangguantongzhi](https://rsshub.app/chinania/renliziyuan/xiangguantongzhi) | [renliziyuan/renshizhaopin](https://rsshub.app/chinania/renliziyuan/renshizhaopin) |
+
+#### [行业统计](https://www.chinania.org.cn/html/hangyetongji/jqzs/)
+
+| [行业分析](https://www.chinania.org.cn/html/hangyetongji/tongji/) | [数据统计](https://www.chinania.org.cn/html/hangyetongji/chanyeshuju/) | [景气指数](https://www.chinania.org.cn/html/hangyetongji/jqzs/) |
+| ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
+| [hangyetongji/tongji](https://rsshub.app/chinania/hangyetongji/tongji) | [hangyetongji/chanyeshuju](https://rsshub.app/chinania/hangyetongji/chanyeshuju) | [hangyetongji/jqzs](https://rsshub.app/chinania/hangyetongji/jqzs) |
+
+#### [政策法规](https://www.chinania.org.cn/html/zcfg/zhengcefagui/)
+
+| [政策法规](https://www.chinania.org.cn/html/zcfg/zhengcefagui/) |
+| ------------------------------------------------------------------ |
+| [zcfg/zhengcefagui](https://rsshub.app/chinania/zcfg/zhengcefagui) |
+
+#### [会议展览](https://www.chinania.org.cn/html/hyzl/huiyizhanlan/)
+
+| [会展通知](https://www.chinania.org.cn/html/hyzl/huiyizhanlan/) | [会展报道](https://www.chinania.org.cn/html/hyzl/huizhanbaodao/) |
+| ------------------------------------------------------------------ | -------------------------------------------------------------------- |
+| [hyzl/huiyizhanlan](https://rsshub.app/chinania/hyzl/huiyizhanlan) | [hyzl/huizhanbaodao](https://rsshub.app/chinania/hyzl/huizhanbaodao) |
+
+更多分类
+
+#### [分支机构信息](http://www.cisia.org/site/term/14.html)
+
+| [企业动态](http://www.cisia.org/site/term/17.html) | [产品展示](http://www.cisia.org/site/term/18.html) |
+| -------------------------------------------------- | -------------------------------------------------- |
+| [17](https://rsshub.app/cisia/17) | [18](https://rsshub.app/cisia/18) |
+
+#### [新闻中心](http://www.cisia.org/site/term/8.html)
+
+| [协会动态](http://www.cisia.org/site/term/9.html) | [行业新闻](http://www.cisia.org/site/term/10.html) | [通知公告](http://www.cisia.org/site/term/11.html) | [市场信息](http://www.cisia.org/site/term/12.html) |
+| ------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
+| [9](https://rsshub.app/cisia/9) | [10](https://rsshub.app/cisia/10) | [11](https://rsshub.app/cisia/11) | [12](https://rsshub.app/cisia/12) |
+
+#### [政策法规](http://www.cisia.org/site/term/19.html)
+
+| [宏观聚焦](http://www.cisia.org/site/term/20.html) | [技术园区](http://www.cisia.org/site/term/396.html) |
+| -------------------------------------------------- | --------------------------------------------------- |
+| [20](https://rsshub.app/cisia/20) | [396](https://rsshub.app/cisia/396) |
+
+#### [合作交流](http://www.cisia.org/site/term/22.html)
+
+| [国际交流](http://www.cisia.org/site/term/23.html) | [行业交流](http://www.cisia.org/site/term/24.html) | [企业调研](http://www.cisia.org/site/term/25.html) | [会展信息](http://www.cisia.org/site/term/84.html) | [宣传专题](http://www.cisia.org/site/term/430.html) |
+| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------- |
+| [23](https://rsshub.app/cisia/23) | [24](https://rsshub.app/cisia/24) | [25](https://rsshub.app/cisia/25) | [84](https://rsshub.app/cisia/84) | [430](https://rsshub.app/cisia/430) |
+
+#### [党建工作](http://www.cisia.org/site/term/26.html)
+
+| [党委文件](http://www.cisia.org/site/term/27.html) | [学习园地](http://www.cisia.org/site/term/28.html) | [两会专题](http://www.cisia.org/site/term/443.html) |
+| -------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------- |
+| [27](https://rsshub.app/cisia/27) | [28](https://rsshub.app/cisia/28) | [443](https://rsshub.app/cisia/443) |
+
+#### [网上服务平台](http://www.cisia.org/site/term/29.html)
+
+| [前沿科技](http://www.cisia.org/site/term/31.html) | [新材料新技术](http://www.cisia.org/site/term/133.html) | [文件共享](http://www.cisia.org/site/term/30.html) |
+| -------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------- |
+| [31](https://rsshub.app/cisia/31) | [133](https://rsshub.app/cisia/133) | [30](https://rsshub.app/cisia/30) |
+
+#### [会员社区](http://www.cisia.org/site/term/34.html)
+
+| [会员分布](http://www.cisia.org/site/term/35.html) | [会员风采](http://www.cisia.org/site/term/68.html) |
+| -------------------------------------------------- | -------------------------------------------------- |
+| [35](https://rsshub.app/cisia/35) | [68](https://rsshub.app/cisia/68) |
+
+
${attachments}`;
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: titleMap.get(cate),
+ link: `https://yjsy.cjlu.edu.cn/index/${cate}.htm`,
+ item: items,
+ };
+}
diff --git a/lib/routes/clickme/namespace.ts b/lib/routes/clickme/namespace.ts
index 2db57538e94147..089498bec5e808 100644
--- a/lib/routes/clickme/namespace.ts
+++ b/lib/routes/clickme/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'ClickMe',
url: 'clickme.net',
+ lang: 'en',
};
diff --git a/lib/routes/cloudnative/namespace.ts b/lib/routes/cloudnative/namespace.ts
index a5e84bcd4f286e..73fa96fdb8df36 100644
--- a/lib/routes/cloudnative/namespace.ts
+++ b/lib/routes/cloudnative/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '云原生社区',
url: 'cloudnative.to',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cls/depth.ts b/lib/routes/cls/depth.ts
index 3d81bb1dd2f19d..9354905de80cab 100644
--- a/lib/routes/cls/depth.ts
+++ b/lib/routes/cls/depth.ts
@@ -47,8 +47,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 头条 | 股市 | 港股 | 环球 | 公司 | 券商 | 基金 | 地产 | 金融 | 汽车 | 科创 | 创业版 | 品见 | 期货 | 投教 |
- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | ---- | ---- | ---- |
- | 1000 | 1003 | 1135 | 1007 | 1005 | 1118 | 1110 | 1006 | 1032 | 1119 | 1111 | 1127 | 1160 | 1124 | 1176 |`,
+| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | ---- | ---- | ---- |
+| 1000 | 1003 | 1135 | 1007 | 1005 | 1118 | 1110 | 1006 | 1032 | 1119 | 1111 | 1127 | 1160 | 1124 | 1176 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cls/namespace.ts b/lib/routes/cls/namespace.ts
index 25ebea7ffbb88f..399323e79488c6 100644
--- a/lib/routes/cls/namespace.ts
+++ b/lib/routes/cls/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '财联社',
url: 'cls.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cls/subject.ts b/lib/routes/cls/subject.ts
new file mode 100644
index 00000000000000..a5db9593040594
--- /dev/null
+++ b/lib/routes/cls/subject.ts
@@ -0,0 +1,153 @@
+import { Route } from '@/types';
+import { getCurrentPath } from '@/utils/helpers';
+const __dirname = getCurrentPath(import.meta.url);
+
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+import { art } from '@/utils/render';
+import path from 'node:path';
+
+import { rootUrl, getSearchParams } 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 currentUrl = new URL(`subject/${id}`, rootUrl).href;
+ const apiUrl = new URL(`api/subject/${id}/article`, rootUrl).href;
+
+ const { data: response } = await got(apiUrl, {
+ searchParams: getSearchParams({
+ Subject_Id: id,
+ }),
+ });
+
+ let items = response.data.slice(0, limit).map((item) => {
+ const title = item.article_title;
+ const description = art(path.join(__dirname, 'templates/description.art'), {
+ intro: item.article_brief,
+ });
+ const guid = `cls-${item.article_id}`;
+ const image = item.article_img;
+
+ return {
+ title,
+ description,
+ pubDate: parseDate(item.article_time, 'X'),
+ link: new URL(`detail/${item.article_id}`, rootUrl).href,
+ category: item.subjects.map((s) => s.subject_name),
+ author: item.article_author,
+ guid,
+ id: guid,
+ content: {
+ html: description,
+ text: item.article_brief,
+ },
+ image,
+ banner: image,
+ };
+ });
+
+ items = await Promise.all(
+ items.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const { data: detailResponse } = await got(item.link);
+
+ const $$ = load(detailResponse);
+
+ const data = JSON.parse($$('script#__NEXT_DATA__').text())?.props?.initialState?.detail?.articleDetail ?? undefined;
+
+ if (!data) {
+ return item;
+ }
+
+ const title = data.title;
+ const description = art(path.join(__dirname, 'templates/description.art'), {
+ images: data.images.map((i) => ({
+ src: i,
+ alt: title,
+ })),
+ intro: data.brief,
+ description: data.content,
+ });
+ const guid = `cls-${data.id}`;
+ const image = data.images?.[0] ?? undefined;
+
+ item.title = title;
+ item.description = description;
+ item.pubDate = parseDate(data.ctime, 'X');
+ item.category = [...new Set(data.subject?.flatMap((s) => [s.name, ...(s.subjectCategory?.flatMap((c) => [c.columnName || [], c.name || []]) ?? [])]) ?? [])].filter(Boolean);
+ item.author = data.author?.name ?? item.author;
+ item.guid = guid;
+ item.id = guid;
+ item.content = {
+ html: description,
+ text: data.content,
+ };
+ item.image = image;
+ item.banner = image;
+ item.enclosure_url = data.audioUrl;
+ item.enclosure_type = item.enclosure_url ? `audio/${item.enclosure_url.split(/\./).pop()}` : undefined;
+ item.enclosure_title = title;
+
+ return item;
+ })
+ )
+ );
+
+ const { data: currentResponse } = await got(currentUrl);
+
+ const $ = load(currentResponse);
+
+ const data = JSON.parse($('script#__NEXT_DATA__').text())?.props?.initialProps?.pageProps?.subjectDetail ?? undefined;
+
+ const author = '财联社';
+ const image = data?.img ?? undefined;
+
+ return {
+ title: `${author} - ${data?.name ?? $('title').text()}`,
+ description: data?.description ?? undefined,
+ link: currentUrl,
+ item: items,
+ allowEmpty: true,
+ image,
+ author,
+ };
+};
+
+export const route: Route = {
+ path: '/subject/:id?',
+ name: '话题',
+ url: 'www.cls.cn',
+ maintainers: ['nczitzk'],
+ handler,
+ example: '/cls/subject/1103',
+ parameters: { category: '分类,默认为 1103,即A股盘面直播,可在对应话题页 URL 中找到' },
+ description: `::: tip
+ 若订阅 [有声早报](https://www.cls.cn/subject/1151),网址为 \`https://www.cls.cn/subject/1151\`。截取 \`https://www.cls.cn/subject/\` 到末尾的部分 \`1151\` 作为参数填入,此时路由为 [\`/cls/subject/1151\`](https://rsshub.app/cls/subject/1151)。
+:::
+ `,
+ categories: ['finance'],
+
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['www.cls.cn/subject/:id'],
+ target: (params) => {
+ const id = params.id;
+
+ return `/subject${id ? `/${id}` : ''}`;
+ },
+ },
+ ],
+};
diff --git a/lib/routes/cls/telegraph.ts b/lib/routes/cls/telegraph.ts
index c5f1b2965e4c20..d831d75ba5dc97 100644
--- a/lib/routes/cls/telegraph.ts
+++ b/lib/routes/cls/telegraph.ts
@@ -44,8 +44,8 @@ export const route: Route = {
handler,
url: 'cls.cn/telegraph',
description: `| 看盘 | 公司 | 解读 | 加红 | 推送 | 提醒 | 基金 | 港股 |
- | ----- | ------------ | ------- | ---- | ----- | ------ | ---- | ---- |
- | watch | announcement | explain | red | jpush | remind | fund | hk |`,
+| ----- | ------------ | ------- | ---- | ----- | ------ | ---- | ---- |
+| watch | announcement | explain | red | jpush | remind | fund | hk |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cls/templates/description.art b/lib/routes/cls/templates/description.art
new file mode 100644
index 00000000000000..249654e7e618a4
--- /dev/null
+++ b/lib/routes/cls/templates/description.art
@@ -0,0 +1,21 @@
+{{ if images }}
+ {{ each images image }}
+ {{ if image?.src }}
+
+
{{ intro }}
+{{ /if }}
+
+{{ if description }}
+ {{@ description }}
+{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/cma/channel.ts b/lib/routes/cma/channel.ts
index fe8944958e7112..5b6aec7f372ace 100644
--- a/lib/routes/cma/channel.ts
+++ b/lib/routes/cma/channel.ts
@@ -27,30 +27,30 @@ export const route: Route = {
handler,
description: `#### 天气实况
- | 频道名称 | 频道 id |
- | -------- | -------------------------------- |
- | 卫星云图 | d3236549863e453aab0ccc4027105bad |
- | 单站雷达 | 103 |
- | 降水量 | 18 |
- | 气温 | 32 |
- | 土壤水分 | 45 |
-
- #### 气象公报
-
- | 频道名称 | 频道 id |
- | -------------- | -------------------------------- |
- | 每日天气提示 | 380 |
- | 重要天气提示 | da5d55817ad5430fb9796a0780178533 |
- | 天气公报 | 3780 |
- | 强对流天气预报 | 383 |
- | 交通气象预报 | 423 |
- | 森林火险预报 | 424 |
- | 海洋天气公报 | 452 |
- | 环境气象公报 | 467 |
-
- :::tip
+| 频道名称 | 频道 id |
+| -------- | -------------------------------- |
+| 卫星云图 | d3236549863e453aab0ccc4027105bad |
+| 单站雷达 | 103 |
+| 降水量 | 18 |
+| 气温 | 32 |
+| 土壤水分 | 45 |
+
+#### 气象公报
+
+| 频道名称 | 频道 id |
+| -------------- | -------------------------------- |
+| 每日天气提示 | 380 |
+| 重要天气提示 | da5d55817ad5430fb9796a0780178533 |
+| 天气公报 | 3780 |
+| 强对流天气预报 | 383 |
+| 交通气象预报 | 423 |
+| 森林火险预报 | 424 |
+| 海洋天气公报 | 452 |
+| 环境气象公报 | 467 |
+
+::: tip
订阅更多细分频道,请前往对应上级频道页,使用下拉菜单选择项目后跳转到目标频道页,查看其 URL 找到对应频道 id
- :::`,
+:::`,
};
async function handler(ctx) {
diff --git a/lib/routes/cma/namespace.ts b/lib/routes/cma/namespace.ts
index e66f2e669d8dfc..876cd450ff2eba 100644
--- a/lib/routes/cma/namespace.ts
+++ b/lib/routes/cma/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国气象局',
url: 'weather.cma.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cmde/namespace.ts b/lib/routes/cmde/namespace.ts
index 7e5f807cba8c5e..da9ba07bd72fc7 100644
--- a/lib/routes/cmde/namespace.ts
+++ b/lib/routes/cmde/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '国家药品监督管理局医疗器械技术审评中心',
url: 'www.cmde.org.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cmpxchg8b/namespace.ts b/lib/routes/cmpxchg8b/namespace.ts
index 08d1eb8396822b..0d26f1ad75bc4f 100644
--- a/lib/routes/cmpxchg8b/namespace.ts
+++ b/lib/routes/cmpxchg8b/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'cmpxchg8b',
url: 'lock.cmpxchg8b.com',
+ lang: 'en',
};
diff --git a/lib/routes/cmu/andypavlo/blog.ts b/lib/routes/cmu/andypavlo/blog.ts
new file mode 100644
index 00000000000000..c31ed07b2a61c6
--- /dev/null
+++ b/lib/routes/cmu/andypavlo/blog.ts
@@ -0,0 +1,55 @@
+import { Route } from '@/types';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+
+async function getArticles() {
+ const url = 'https://www.cs.cmu.edu/~pavlo/blog/index.html';
+ const { data: res } = await got(url);
+ const $ = load(res);
+
+ const list = $('.row.mb-3')
+ .toArray()
+ .map((element) => {
+ const $item = $(element);
+ const $title = $item.find('h4 a');
+ const $date = $item.find('.text-muted');
+ const $description = $item.find('p');
+
+ return {
+ title: $title.text().trim(),
+ link: $title.attr('href'),
+ description: $description.text().trim(),
+ pubDate: parseDate($date.attr('title')),
+ guid: $title.attr('href'),
+ };
+ });
+ return list;
+}
+
+export const route: Route = {
+ path: '/andypavlo/blog',
+ categories: ['blog'],
+ example: '/cmu/andypavlo/blog',
+ parameters: {},
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ name: 'Andy Pavlo Blog',
+ maintainers: ['mocusez'],
+ handler,
+};
+
+async function handler() {
+ const articles = await getArticles();
+ return {
+ title: 'Andy Pavlo - Carnegie Mellon University',
+ link: 'https://www.cs.cmu.edu/~pavlo/blog/index.html',
+ item: articles,
+ };
+}
diff --git a/lib/routes/cmu/namespace.ts b/lib/routes/cmu/namespace.ts
new file mode 100644
index 00000000000000..5bb54310581c72
--- /dev/null
+++ b/lib/routes/cmu/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Carnegie Mellon University',
+ url: 'www.cmu.edu',
+ lang: 'en',
+};
diff --git a/lib/routes/cn-healthcare/namespace.ts b/lib/routes/cn-healthcare/namespace.ts
index ca76d162687225..aaaef82c3d5180 100644
--- a/lib/routes/cn-healthcare/namespace.ts
+++ b/lib/routes/cn-healthcare/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '健康界',
url: 'cn-healthcare.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cna/index.ts b/lib/routes/cna/index.ts
index aa52841a9258ac..d5e219a9eca834 100644
--- a/lib/routes/cna/index.ts
+++ b/lib/routes/cna/index.ts
@@ -22,8 +22,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 即時 | 政治 | 國際 | 兩岸 | 產經 | 證券 | 科技 | 生活 | 社會 | 地方 | 文化 | 運動 | 娛樂 |
- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
- | aall | aipl | aopl | acn | aie | asc | ait | ahel | asoc | aloc | acul | aspt | amov |`,
+| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
+| aall | aipl | aopl | acn | aie | asc | ait | ahel | asoc | aloc | acul | aspt | amov |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cna/namespace.ts b/lib/routes/cna/namespace.ts
index 73b8155d68d77b..b34e748a72b763 100644
--- a/lib/routes/cna/namespace.ts
+++ b/lib/routes/cna/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中央通讯社',
url: 'cna.com.tw',
+ lang: 'zh-TW',
};
diff --git a/lib/routes/cnbc/namespace.ts b/lib/routes/cnbc/namespace.ts
index 6243dcb5137059..b259fa6783dc8f 100644
--- a/lib/routes/cnbc/namespace.ts
+++ b/lib/routes/cnbc/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'CNBC',
url: 'search.cnbc.com',
+ lang: 'en',
};
diff --git a/lib/routes/cnbc/rss.ts b/lib/routes/cnbc/rss.ts
index 5e81b99d02aebb..ecc6620f1d59bb 100644
--- a/lib/routes/cnbc/rss.ts
+++ b/lib/routes/cnbc/rss.ts
@@ -65,7 +65,7 @@ async function handler(ctx) {
}
const meta = JSON.parse($('[type=application/ld+json]').last().text());
- item.author = meta.author ? meta.author.name ?? meta.author.map((a) => a.name).join(', ') : null;
+ item.author = meta.author ? (meta.author.name ?? meta.author.map((a) => a.name).join(', ')) : null;
item.category = meta.keywords;
return item;
diff --git a/lib/routes/cnbeta/category.ts b/lib/routes/cnbeta/category.ts
new file mode 100644
index 00000000000000..a873cb07837505
--- /dev/null
+++ b/lib/routes/cnbeta/category.ts
@@ -0,0 +1,23 @@
+import { Route } from '@/types';
+import { handler } from './common';
+
+export const route: Route = {
+ name: '分类',
+ path: ['/category/:id'],
+ example: '/cnbeta/category/movie',
+ maintainers: ['nczitzk'],
+ parameters: {
+ id: '分类 id,可在对应分类页的 URL 中找到',
+ },
+ radar: [
+ {
+ source: ['cnbeta.com.tw/category/:id'],
+ target: (params) => `/cnbeta/category/${params.id.replace('.htm', '')}`,
+ },
+ ],
+ handler,
+ url: 'cnbeta.com.tw',
+ description: `| 影视 | 音乐 | 游戏 | 动漫 | 趣闻 | 科学 | 软件 |
+| ----- | ----- | ---- | ----- | ----- | ------- | ---- |
+| movie | music | game | comic | funny | science | soft |`,
+};
diff --git a/lib/routes/cnbeta/type.ts b/lib/routes/cnbeta/common.ts
similarity index 81%
rename from lib/routes/cnbeta/type.ts
rename to lib/routes/cnbeta/common.ts
index ea3b5383cdb195..5f4af27818e5b6 100644
--- a/lib/routes/cnbeta/type.ts
+++ b/lib/routes/cnbeta/common.ts
@@ -1,4 +1,3 @@
-import { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
@@ -7,22 +6,7 @@ import { parseDate } from '@/utils/parse-date';
import { rootUrl, ProcessItems } from './utils';
-export const route: Route = {
- path: ['/:type/:id', '/'],
- radar: [
- {
- source: ['cnbeta.com.tw/'],
- target: '',
- },
- ],
- name: 'Unknown',
- maintainers: [],
- handler,
- url: 'cnbeta.com.tw/',
- url: 'cnbeta.com.tw/',
-};
-
-async function handler(ctx) {
+export async function handler(ctx) {
const { type, id } = ctx.req.param();
const currentUrl = type ? `${rootUrl}/${type}/${id}.htm` : rootUrl;
diff --git a/lib/routes/cnbeta/index.ts b/lib/routes/cnbeta/index.ts
new file mode 100644
index 00000000000000..9d7f3012e7e547
--- /dev/null
+++ b/lib/routes/cnbeta/index.ts
@@ -0,0 +1,16 @@
+import { Route } from '@/types';
+import { handler } from './common';
+
+export const route: Route = {
+ name: '头条资讯',
+ path: ['/'],
+ example: '/cnbeta',
+ radar: [
+ {
+ source: ['cnbeta.com.tw/'],
+ },
+ ],
+ maintainers: ['kt286', 'HaitianLiu', 'nczitzk'],
+ handler,
+ url: 'cnbeta.com.tw',
+};
diff --git a/lib/routes/cnbeta/namespace.ts b/lib/routes/cnbeta/namespace.ts
index af50033d377051..d703fd32ee9c3a 100644
--- a/lib/routes/cnbeta/namespace.ts
+++ b/lib/routes/cnbeta/namespace.ts
@@ -3,4 +3,6 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'cnBeta.COM',
url: 'cnbeta.com.tw',
+ categories: ['new-media', 'popular'],
+ lang: 'zh-TW',
};
diff --git a/lib/routes/cnbeta/topics.ts b/lib/routes/cnbeta/topics.ts
new file mode 100644
index 00000000000000..84dc83eaf66f09
--- /dev/null
+++ b/lib/routes/cnbeta/topics.ts
@@ -0,0 +1,23 @@
+import { Route } from '@/types';
+import { handler } from './common';
+
+export const route: Route = {
+ name: '主题',
+ path: ['/topics/:id'],
+ example: '/cnbeta/topics/453',
+ maintainers: ['cczhong11', 'nczitzk'],
+ parameters: {
+ id: '主题 id,可在对应主题页的 URL 中找到',
+ },
+ radar: [
+ {
+ source: ['cnbeta.com.tw/topics/:id'],
+ target: (params) => `/cnbeta/topics/${params.id.replace('.htm', '')}`,
+ },
+ ],
+ handler,
+ url: 'cnbeta.com.tw',
+ description: `::: tip
+完整的主题列表参见 [主题列表](https://www.cnbeta.com.tw/topics.htm)
+:::`,
+};
diff --git a/lib/routes/cnblogs/common.ts b/lib/routes/cnblogs/common.ts
index 9277ab20c1f98e..456c8ad6678989 100644
--- a/lib/routes/cnblogs/common.ts
+++ b/lib/routes/cnblogs/common.ts
@@ -28,9 +28,6 @@ export const route: Route = {
handler,
url: 'www.cnblogs.com/pick',
description: `在博客园主页的分类出可查看所有类型。例如,go 的分类地址为: \`https://www.cnblogs.com/cate/go/\`, 则: [\`/cnblogs/cate/go\`](https://rsshub.app/cnblogs/cate/go)`,
- url: 'www.cnblogs.com/aggsite/headline',
- url: 'www.cnblogs.com/aggsite/topviews',
- url: 'www.cnblogs.com/aggsite/topdiggs',
};
async function handler(ctx) {
diff --git a/lib/routes/cnblogs/namespace.ts b/lib/routes/cnblogs/namespace.ts
index c7434c16ae9cba..055c2a06967560 100644
--- a/lib/routes/cnblogs/namespace.ts
+++ b/lib/routes/cnblogs/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '博客园',
url: 'www.cnblogs.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cncf/index.ts b/lib/routes/cncf/index.ts
index 409bfcc82061c3..acffbbf6cf05ca 100644
--- a/lib/routes/cncf/index.ts
+++ b/lib/routes/cncf/index.ts
@@ -23,8 +23,8 @@ export const route: Route = {
maintainers: ['Fatpandac'],
handler,
description: `| Blog | News | Announcements | Reports |
- | ---- | ---- | ------------- | ------- |
- | blog | news | announcements | reports |`,
+| ---- | ---- | ------------- | ------- |
+| blog | news | announcements | reports |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cncf/namespace.ts b/lib/routes/cncf/namespace.ts
index 87bbbe17832996..a479718a158368 100644
--- a/lib/routes/cncf/namespace.ts
+++ b/lib/routes/cncf/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'CNCF',
url: 'cncf.io',
+ lang: 'en',
};
diff --git a/lib/routes/cneb/namespace.ts b/lib/routes/cneb/namespace.ts
index 7a03e739e19b99..06ffe10bf6fa37 100644
--- a/lib/routes/cneb/namespace.ts
+++ b/lib/routes/cneb/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '中国国家应急广播',
url: 'cneb.gov.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cneb/yjxw.ts b/lib/routes/cneb/yjxw.ts
index d490824277afac..650ad57b4dad89 100644
--- a/lib/routes/cneb/yjxw.ts
+++ b/lib/routes/cneb/yjxw.ts
@@ -32,8 +32,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 全部 | 国内新闻 | 国际新闻 |
- | ---- | -------- | -------- |
- | | gnxw | gjxw |`,
+| ---- | -------- | -------- |
+| | gnxw | gjxw |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cngal/namespace.ts b/lib/routes/cngal/namespace.ts
index a562b96ab2dd2e..f5f6dbd51b00e9 100644
--- a/lib/routes/cngal/namespace.ts
+++ b/lib/routes/cngal/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'CnGal',
url: 'www.cngal.org',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cngal/weekly.ts b/lib/routes/cngal/weekly.ts
index 0ee1e8eb317e6a..487e7e8010b386 100644
--- a/lib/routes/cngal/weekly.ts
+++ b/lib/routes/cngal/weekly.ts
@@ -1,4 +1,4 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import { getCurrentPath } from '@/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);
@@ -9,7 +9,8 @@ import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/weekly',
- categories: ['anime'],
+ categories: ['anime', 'popular'],
+ view: ViewType.Articles,
example: '/cngal/weekly',
parameters: {},
features: {
@@ -31,7 +32,7 @@ export const route: Route = {
url: 'www.cngal.org/',
};
-async function handler(ctx) {
+async function handler() {
const response = await got('https://www.cngal.org/api/news/GetWeeklyNewsOverview');
return {
@@ -44,5 +45,4 @@ async function handler(ctx) {
link: `https://www.cngal.org/articles/index/${item.id}`,
})),
};
- ctx.state.json = response.data;
}
diff --git a/lib/routes/cngold/index.ts b/lib/routes/cngold/index.ts
new file mode 100644
index 00000000000000..955fa46e0a49c3
--- /dev/null
+++ b/lib/routes/cngold/index.ts
@@ -0,0 +1,195 @@
+import { Route } from '@/types';
+
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+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 rootUrl = 'https://www.cngold.org.cn';
+ const currentUrl = new URL(`${category}.html`, rootUrl).href;
+
+ const { data: response } = await got(currentUrl);
+
+ const $ = load(response);
+
+ const language = $('html').prop('lang');
+
+ let items = $('ul.newsList li')
+ .slice(0, limit)
+ .toArray()
+ .map((item) => {
+ item = $(item);
+
+ return {
+ title: item.find('t1').text(),
+ pubDate: parseDate(item.find('div.min, div.day').text(), ['YYYY-MM-DD', 'MM-DD']),
+ link: new URL(item.find('a').prop('href'), rootUrl).href,
+ language,
+ };
+ });
+
+ items = await Promise.all(
+ items.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const { data: detailResponse } = await got(item.link);
+
+ const $$ = load(detailResponse);
+
+ const title = $$('div.details_top div.t1').text();
+ const description = $$('div.details_con').html();
+
+ item.title = title;
+ item.description = description;
+ item.pubDate = parseDate($$('div.details_top div.min span').first().text());
+ item.author = $$('div.details_top div.min span').last().text().split(/:/).pop();
+ item.content = {
+ html: description,
+ text: $$('div.details_con').text(),
+ };
+ item.language = language;
+
+ return item;
+ })
+ )
+ );
+
+ const image = new URL($('div.logo img').prop('src'), rootUrl).href;
+
+ return {
+ title: `${$('title').text()} - ${$('div.tab a.current').text()}`,
+ description: $('meta[name="description"]').prop('content'),
+ link: currentUrl,
+ item: items,
+ allowEmpty: true,
+ image,
+ author: $('meta[name="keywords"]').prop('content'),
+ language,
+ };
+};
+
+export const route: Route = {
+ path: '/:category?',
+ name: '分类',
+ url: 'www.cngold.org.cn',
+ maintainers: ['nczitzk'],
+ handler,
+ example: '/cngold/news-325',
+ parameters: { category: '分类,默认为 `news-325`,即行业资讯,可在对应分类页 URL 中找到, Category, `news-325`,即行业资讯by default' },
+ description: `::: tip
+ 若订阅 [行业资讯](https://www.cngold.org.cn/news-325.html),网址为 \`https://www.cngold.org.cn/news-325.html\`。截取 \`https://www.cngold.org.cn/\` 到末尾 \`.html\` 的部分 \`news-325\` 作为参数填入,此时路由为 [\`/cngold/news-325\`](https://rsshub.app/cngold/news-325)。
+:::
+
+#### 资讯中心
+
+| [图片新闻](https://www.cngold.org.cn/news-323.html) | [通知公告](https://www.cngold.org.cn/news-324.html) | [党建工作](https://www.cngold.org.cn/news-326.html) | [行业资讯](https://www.cngold.org.cn/news-325.html) | [黄金矿业](https://www.cngold.org.cn/news-327.html) | [黄金消费](https://www.cngold.org.cn/news-328.html) |
+| --------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- |
+| [news-323](https://rsshub.app/cngold/news-323) | [news-324](https://rsshub.app/cngold/news-324) | [news-326](https://rsshub.app/cngold/news-326) | [news-325](https://rsshub.app/cngold/news-325) | [news-327](https://rsshub.app/cngold/news-327) | [news-328](https://rsshub.app/cngold/news-328) |
+
+| [黄金市场](https://www.cngold.org.cn/news-329.html) | [社会责任](https://www.cngold.org.cn/news-330.html) | [黄金书屋](https://www.cngold.org.cn/news-331.html) | [工作交流](https://www.cngold.org.cn/news-332.html) | [黄金统计](https://www.cngold.org.cn/news-333.html) | [协会动态](https://www.cngold.org.cn/news-334.html) |
+| --------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------- |
+| [news-329](https://rsshub.app/cngold/news-329) | [news-330](https://rsshub.app/cngold/news-330) | [news-331](https://rsshub.app/cngold/news-331) | [news-332](https://rsshub.app/cngold/news-332) | [news-333](https://rsshub.app/cngold/news-333) | [news-334](https://rsshub.app/cngold/news-334) |
+
+更多分类
+
+#### [政策法规](https://www.cngold.org.cn/policies.html)
+
+| [法律法规](https://www.cngold.org.cn/policies-245.html) | [产业政策](https://www.cngold.org.cn/policies-262.html) | [黄金标准](https://www.cngold.org.cn/policies-281.html) |
+| ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- |
+| [policies-245](https://rsshub.app/cngold/policies-245) | [policies-262](https://rsshub.app/cngold/policies-262) | [policies-281](https://rsshub.app/cngold/policies-281) |
+
+#### [行业培训](https://www.cngold.org.cn/training.html)
+
+| [黄金投资分析师](https://www.cngold.org.cn/training-242.html) | [教育部1+X](https://www.cngold.org.cn/training-246.html) | [矿业权评估师](https://www.cngold.org.cn/training-338.html) | [其他培训](https://www.cngold.org.cn/training-247.html) |
+| ------------------------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------- |
+| [training-242](https://rsshub.app/cngold/training-242) | [training-246](https://rsshub.app/cngold/training-246) | [training-338](https://rsshub.app/cngold/training-338) | [training-247](https://rsshub.app/cngold/training-247) |
+
+#### [黄金科技](https://www.cngold.org.cn/technology.html)
+
+| [黄金协会科学技术奖](https://www.cngold.org.cn/technology-318.html) | [科学成果评价](https://www.cngold.org.cn/technology-319.html) | [新技术推广](https://www.cngold.org.cn/technology-320.html) | [黄金技术大会](https://www.cngold.org.cn/technology-350.html) |
+| ------------------------------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------- |
+| [technology-318](https://rsshub.app/cngold/technology-318) | [technology-319](https://rsshub.app/cngold/technology-319) | [technology-320](https://rsshub.app/cngold/technology-320) | [technology-350](https://rsshub.app/cngold/technology-350) |
+
+
+ {{ else if file.extension === 'm4a' || file.extension === 'mp3' || file.extension === 'ogg' }}
+
+ {{ else if file.extension === 'mp4' || file.extension === 'webm' }}
+
+ {{ else }}
+ {{file.name}}
+ {{ /if }}
+ {{ /each }}
+{{ /if }}
+
+{{ if i.embed }}
+ {{ if i.embed.type === 'image' }}
+
+ {{ else if i.embed.type === 'link' }}
+ {{ if i.embed.thumbnail }}
+
+ {{ /if }}
+ {{ i.embed.title }}{{ if i.embed.description }}
More languages
+
+| 语言代码 | 语言名称 |
+| ------------------------------------------------- | ---------- |
+| English | english |
+| Español - España (Spanish - Spain) | spanish |
+| Français (French) | french |
+| Italiano (Italian) | italian |
+| Deutsch (German) | german |
+| Ελληνικά (Greek) | greek |
+| 한국어 (Korean) | koreana |
+| 简体中文 (Simplified Chinese) | schinese |
+| 繁體中文 (Traditional Chinese) | tchinese |
+| Русский (Russian) | russian |
+| ไทย (Thai) | thai |
+| 日本語 (Japanese) | japanese |
+| Português (Portuguese) | portuguese |
+| Português - Brasil (Portuguese - Brazil) | brazilian |
+| Polski (Polish) | polish |
+| Dansk (Danish) | danish |
+| Nederlands (Dutch) | dutch |
+| Suomi (Finnish) | finnish |
+| Norsk (Norwegian) | norwegian |
+| Svenska (Swedish) | swedish |
+| Čeština (Czech) | czech |
+| Magyar (Hungarian) | hungarian |
+| Română (Romanian) | romanian |
+| Български (Bulgarian) | bulgarian |
+| Türkçe (Turkish) | turkish |
+| Українська (Ukrainian) | ukrainian |
+| Tiếng Việt (Vietnamese) | vietnamese |
+| Español - Latinoamérica (Spanish - Latin America) | latam |
+
+更多栏目
+更多栏目
- #### 要闻
+#### 要闻
- | 财经要闻 | 观点评论 | 民生消费 |
- | -------- | -------- | --------- |
- | xwzx/hg | xwzx/jr | xwzx/msxf |
+| 财经要闻 | 观点评论 | 民生消费 |
+| -------- | -------- | --------- |
+| xwzx/hg | xwzx/jr | xwzx/msxf |
- #### 公司
+#### 公司
- | 公司要闻 | 公司深度 | 公司巡礼 |
- | --------- | --------- | --------- |
- | ssgs/gsxw | ssgs/gssd | ssgs/gsxl |
+| 公司要闻 | 公司深度 | 公司巡礼 |
+| --------- | --------- | --------- |
+| ssgs/gsxw | ssgs/gssd | ssgs/gsxl |
- #### 市场
+#### 市场
- | A 股市场 | 港股资讯 | 债市研究 | 海外报道 | 期货报道 |
- | --------- | --------- | --------- | --------- | --------- |
- | gppd/gsyj | gppd/ggzx | gppd/zqxw | gppd/hwbd | gppd/qhbd |
+| A 股市场 | 港股资讯 | 债市研究 | 海外报道 | 期货报道 |
+| --------- | --------- | --------- | --------- | --------- |
+| gppd/gsyj | gppd/ggzx | gppd/zqxw | gppd/hwbd | gppd/qhbd |
- #### 基金
+#### 基金
- | 基金动态 | 基金视点 | 基金持仓 | 私募基金 | 基民学苑 |
- | --------- | --------- | --------- | --------- | --------- |
- | tzjj/jjdt | tzjj/jjks | tzjj/jjcs | tzjj/smjj | tzjj/tjdh |
+| 基金动态 | 基金视点 | 基金持仓 | 私募基金 | 基民学苑 |
+| --------- | --------- | --------- | --------- | --------- |
+| tzjj/jjdt | tzjj/jjks | tzjj/jjcs | tzjj/smjj | tzjj/tjdh |
- #### 机构
+#### 机构
- | 券商 | 银行 | 保险 |
- | ---- | ---- | ---- |
- | qs | yh | bx |
+| 券商 | 银行 | 保险 |
+| ---- | ---- | ---- |
+| qs | yh | bx |
- #### 其他
+#### 其他
- | 中证快讯 7x24 | IPO 鉴真 | 公司能见度 |
- | ------------- | -------- | ---------- |
- | sylm/jsbd | yc/ipojz | yc/gsnjd |
-
+
${$.html()}`;
+
+ return {
+ title: item.field_5,
+ description,
+ pubDate: parseDate(item.field_2.iso_timestamp),
+ link,
+ guid: `cybersecurityventures:${item.id}`,
+ } as DataItem;
+ }),
+ };
+}
diff --git a/lib/routes/cybersecurityventures/types.ts b/lib/routes/cybersecurityventures/types.ts
new file mode 100644
index 00000000000000..8440b884946461
--- /dev/null
+++ b/lib/routes/cybersecurityventures/types.ts
@@ -0,0 +1,17 @@
+export interface RawRecord {
+ id: string;
+ field_2: {
+ date: string;
+ date_formatted: string;
+ hours: string;
+ minutes: string;
+ am_pm: string;
+ unix_timestamp: number;
+ iso_timestamp: string;
+ timestamp: string;
+ time: number;
+ };
+ field_3: string;
+ field_4: string;
+ field_5: string;
+}
diff --git a/lib/routes/cyzone/author.ts b/lib/routes/cyzone/author.ts
index 1c75e31bfda411..dfa0254fe1ed35 100644
--- a/lib/routes/cyzone/author.ts
+++ b/lib/routes/cyzone/author.ts
@@ -4,7 +4,7 @@ import { rootUrl, apiRootUrl, processItems, getInfo } from './util';
export const route: Route = {
path: '/author/:id',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/cyzone/author/1225562',
parameters: { id: '作者 id,可在对应作者页 URL 中找到' },
features: {
diff --git a/lib/routes/cyzone/index.ts b/lib/routes/cyzone/index.ts
index 11cf7f8bc797a7..92683285ae4c95 100644
--- a/lib/routes/cyzone/index.ts
+++ b/lib/routes/cyzone/index.ts
@@ -14,16 +14,16 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 最新 | 快鲤鱼 | 创投 | 科创板 | 汽车 |
- | ---- | ------ | ---- | ------ | ---- |
- | news | 5 | 14 | 13 | 8 |
+| ---- | ------ | ---- | ------ | ---- |
+| news | 5 | 14 | 13 | 8 |
- | 海外 | 消费 | 科技 | 医疗 | 文娱 |
- | ---- | ---- | ---- | ---- | ---- |
- | 10 | 9 | 7 | 27 | 11 |
+| 海外 | 消费 | 科技 | 医疗 | 文娱 |
+| ---- | ---- | ---- | ---- | ---- |
+| 10 | 9 | 7 | 27 | 11 |
- | 城市 | 政策 | 特写 | 干货 | 科技股 |
- | ---- | ---- | ---- | ---- | ------ |
- | 16 | 15 | 6 | 12 | 33 |`,
+| 城市 | 政策 | 特写 | 干货 | 科技股 |
+| ---- | ---- | ---- | ---- | ------ |
+| 16 | 15 | 6 | 12 | 33 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/cyzone/label.ts b/lib/routes/cyzone/label.ts
index 9defe810e88ada..7259e5e83d05e0 100644
--- a/lib/routes/cyzone/label.ts
+++ b/lib/routes/cyzone/label.ts
@@ -4,7 +4,7 @@ import { rootUrl, apiRootUrl, processItems, getInfo } from './util';
export const route: Route = {
path: '/label/:name',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/cyzone/label/创业邦周报',
parameters: { name: '标签名称,可在对应标签页 URL 中找到' },
features: {
diff --git a/lib/routes/cyzone/namespace.ts b/lib/routes/cyzone/namespace.ts
index e9a9f2c62c7657..5a92c11f3f0d05 100644
--- a/lib/routes/cyzone/namespace.ts
+++ b/lib/routes/cyzone/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '创业邦',
url: 'cyzone.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/cztv/namespace.ts b/lib/routes/cztv/namespace.ts
index 2c7a7f915fa825..04f6f4434f1bbc 100644
--- a/lib/routes/cztv/namespace.ts
+++ b/lib/routes/cztv/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '新蓝网(浙江广播电视集团)',
url: 'cztv.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/dahecube/index.ts b/lib/routes/dahecube/index.ts
index 1f3c7794e40cd9..16d5a8bcfe5b00 100644
--- a/lib/routes/dahecube/index.ts
+++ b/lib/routes/dahecube/index.ts
@@ -22,8 +22,8 @@ export const route: Route = {
maintainers: ['linbuxiao'],
handler,
description: `| 推荐 | 党史 | 豫股 | 财经 | 投教 | 金融 | 科创 | 投融 | 专栏 |
- | --------- | ------- | ----- | -------- | --------- | ------- | ------- | ------ | ------ |
- | recommend | history | stock | business | education | finance | science | invest | column |`,
+| --------- | ------- | ----- | -------- | --------- | ------- | ------- | ------ | ------ |
+| recommend | history | stock | business | education | finance | science | invest | column |`,
};
async function handler(ctx) {
diff --git a/lib/routes/dahecube/namespace.ts b/lib/routes/dahecube/namespace.ts
index 44cf03e514201c..f99b69a0b7deb9 100644
--- a/lib/routes/dahecube/namespace.ts
+++ b/lib/routes/dahecube/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '大河财立方',
url: 'dahecube.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/daily/discussed.ts b/lib/routes/daily/discussed.ts
index 54a5957276b6d4..cdc7027bf79bf6 100644
--- a/lib/routes/daily/discussed.ts
+++ b/lib/routes/daily/discussed.ts
@@ -1,9 +1,5 @@
-import { Route } from '@/types';
-import { getData, getList, getRedirectedLink } from './utils.js';
-
-const variables = {
- first: 15,
-};
+import { Route, ViewType } from '@/types';
+import { baseUrl, getData, getList, variables } from './utils.js';
const query = `
query MostDiscussedFeed(
@@ -19,6 +15,7 @@ const query = `
edges {
node {
...FeedPost
+ contentHtml
}
}
}
@@ -33,6 +30,7 @@ const query = `
image
readTime
permalink
+ commentsPermalink
summary
createdAt
numUpvotes
@@ -52,37 +50,74 @@ const query = `
bio
}
`;
-const graphqlQuery = {
- query,
- variables,
-};
export const route: Route = {
- path: '/discussed',
- example: '/daily/discussed',
+ path: '/discussed/:period?/:innerSharedContent?/:dateSort?',
+ example: '/daily/discussed/30',
+ view: ViewType.Articles,
radar: [
{
- source: ['daily.dev/popular'],
+ source: ['app.daily.dev/discussed'],
},
],
name: 'Most Discussed',
maintainers: ['Rjnishant530'],
handler,
- url: 'daily.dev/popular',
+ url: 'app.daily.dev/discussed',
+ parameters: {
+ innerSharedContent: {
+ description: 'Where to Fetch inner Shared Posts instead of original',
+ default: 'false',
+ options: [
+ { value: 'false', label: 'False' },
+ { value: 'true', label: 'True' },
+ ],
+ },
+ dateSort: {
+ description: 'Sort posts by publication date instead of popularity',
+ default: 'true',
+ options: [
+ { value: 'false', label: 'False' },
+ { value: 'true', label: 'True' },
+ ],
+ },
+ period: {
+ description: 'Period of Lookup',
+ default: '7',
+ options: [
+ { value: '7', label: 'Last Week' },
+ { value: '30', label: 'Last Month' },
+ { value: '365', label: 'Last Year' },
+ ],
+ },
+ },
};
-async function handler() {
- const baseUrl = 'https://app.daily.dev/discussed';
- const data = await getData(graphqlQuery);
- const list = getList(data);
- const items = await getRedirectedLink(list);
+async function handler(ctx) {
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 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 link = `${baseUrl}/posts/discussed`;
+
+ const data = await getData({
+ query,
+ variables: {
+ ...variables,
+ first: limit,
+ period,
+ },
+ });
+ const items = getList(data, innerSharedContent, dateSort);
+
return {
- title: 'Most Discussed',
- link: baseUrl,
+ title: 'Real-time discussions in the developer community | daily.dev',
+ link,
item: items,
- description: 'Most Discussed Posts on Daily.dev',
- logo: 'https://app.daily.dev/favicon-32x32.png',
- icon: 'https://app.daily.dev/favicon-32x32.png',
+ description: 'Stay on top of real-time developer discussions on daily.dev. Join conversations happening now and engage with the most active community members.',
+ logo: `${baseUrl}/favicon-32x32.png`,
+ icon: `${baseUrl}/favicon-32x32.png`,
language: 'en-us',
};
}
diff --git a/lib/routes/daily/index.ts b/lib/routes/daily/index.ts
deleted file mode 100644
index 031f47c0a773eb..00000000000000
--- a/lib/routes/daily/index.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { Route } from '@/types';
-import { getData, getList, getRedirectedLink } from './utils.js';
-
-const variables = {
- version: 11,
- ranking: 'POPULARITY',
- first: 15,
-};
-
-const query = `
- query AnonymousFeed(
- $first: Int
- $ranking: Ranking
- $version: Int
- $supportedTypes: [String!] = ["article","share","freeform"]
- ) {
- page: anonymousFeed(
- first: $first
- ranking: $ranking
- version: $version
- supportedTypes: $supportedTypes
- ) {
- ...FeedPostConnection
- }
- }
-
- fragment FeedPostConnection on PostConnection {
- edges {
- node {
- ...FeedPost
- }
- }
- }
-
- fragment FeedPost on Post {
- ...SharedPostInfo
- }
-
- fragment SharedPostInfo on Post {
- id
- title
- image
- readTime
- permalink
- summary
- createdAt
- numUpvotes
- numComments
- author {
- ...UserShortInfo
- }
- tags
- }
-
- fragment UserShortInfo on User {
- id
- name
- image
- permalink
- username
- bio
- }
-`;
-
-const graphqlQuery = {
- query,
- variables,
-};
-
-export const route: Route = {
- path: '/',
- example: '/daily',
- radar: [
- {
- source: ['daily.dev/popular'],
- },
- ],
- name: 'Popular',
- maintainers: ['Rjnishant530'],
- handler,
- url: 'daily.dev/popular',
-};
-
-async function handler() {
- const baseUrl = 'https://app.daily.dev/popular';
- const data = await getData(graphqlQuery);
- const list = getList(data);
- const items = await getRedirectedLink(list);
- return {
- title: 'Popular',
- link: baseUrl,
- item: items,
- description: 'Popular Posts on Daily.dev',
- logo: 'https://app.daily.dev/favicon-32x32.png',
- icon: 'https://app.daily.dev/favicon-32x32.png',
- language: 'en-us',
- };
-}
diff --git a/lib/routes/daily/namespace.ts b/lib/routes/daily/namespace.ts
index 2dbd9bf58eba9c..1b57d03d51c40a 100644
--- a/lib/routes/daily/namespace.ts
+++ b/lib/routes/daily/namespace.ts
@@ -2,6 +2,7 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Daily.dev',
- url: 'daily.dev',
+ url: 'app.daily.dev',
categories: ['social-media'],
+ lang: 'en',
};
diff --git a/lib/routes/daily/popular.ts b/lib/routes/daily/popular.ts
new file mode 100644
index 00000000000000..32e654ec7f282c
--- /dev/null
+++ b/lib/routes/daily/popular.ts
@@ -0,0 +1,182 @@
+import { Route, ViewType } from '@/types';
+import { baseUrl, getData, getList, variables } from './utils.js';
+
+const query = `
+ query AnonymousFeed(
+ $loggedIn: Boolean! = false
+ $first: Int
+ $after: String
+ $ranking: Ranking
+ $version: Int
+ $supportedTypes: [String!] = ["article","share","freeform","video:youtube","collection"]
+ ) {
+ page: anonymousFeed(
+ first: $first
+ after: $after
+ ranking: $ranking
+ version: $version
+ supportedTypes: $supportedTypes
+ ) {
+ ...FeedPostConnection
+ }
+ }
+
+ fragment FeedPostConnection on PostConnection {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ edges {
+ node {
+ ...FeedPost
+ contentHtml
+ ...UserPost @include(if: $loggedIn)
+ }
+ }
+ }
+
+ fragment FeedPost on Post {
+ ...FeedPostInfo
+ sharedPost {
+ id
+ title
+ image
+ readTime
+ permalink
+ commentsPermalink
+ createdAt
+ type
+ tags
+ source {
+ id
+ handle
+ permalink
+ image
+ }
+ slug
+ clickbaitTitleDetected
+ }
+ trending
+ feedMeta
+ collectionSources {
+ handle
+ image
+ }
+ numCollectionSources
+ updatedAt
+ slug
+ }
+
+
+ fragment FeedPostInfo on Post {
+ id
+ title
+ image
+ readTime
+ permalink
+ commentsPermalink
+ createdAt
+ commented
+ bookmarked
+ views
+ numUpvotes
+ numComments
+ summary
+ bookmark {
+ remindAt
+ }
+ author {
+ id
+ name
+ image
+ username
+ permalink
+ }
+ type
+ tags
+ source {
+ id
+ handle
+ name
+ permalink
+ image
+ type
+ }
+ userState {
+ vote
+ flags {
+ feedbackDismiss
+ }
+ }
+ slug
+ clickbaitTitleDetected
+ }
+
+ fragment UserPost on Post {
+ read
+ upvoted
+ commented
+ bookmarked
+ downvoted
+ }
+`;
+
+export const route: Route = {
+ path: '/popular/:innerSharedContent?/:dateSort?',
+ example: '/daily/popular',
+ view: ViewType.Articles,
+ radar: [
+ {
+ source: ['app.daily.dev/popular'],
+ },
+ ],
+ parameters: {
+ innerSharedContent: {
+ description: 'Where to Fetch inner Shared Posts instead of original',
+ default: 'false',
+ options: [
+ { value: 'false', label: 'False' },
+ { value: 'true', label: 'True' },
+ ],
+ },
+ dateSort: {
+ description: 'Sort posts by publication date instead of popularity',
+ default: 'true',
+ options: [
+ { value: 'false', label: 'False' },
+ { value: 'true', label: 'True' },
+ ],
+ },
+ },
+ name: 'Popular',
+ maintainers: ['Rjnishant530'],
+ handler,
+ url: 'app.daily.dev/popular',
+};
+
+async function handler(ctx) {
+ const link = `${baseUrl}/posts`;
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 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;
+
+ const data = await getData({
+ query,
+ variables: {
+ ...variables,
+ ranking: 'POPULARITY',
+ first: limit,
+ },
+ });
+ const items = getList(data, innerSharedContent, dateSort);
+
+ return {
+ title: 'Popular posts on daily.dev',
+ link,
+ item: items,
+ description: 'daily.dev is the easiest way to stay updated on the latest programming news. Get the best content from the top tech publications on any topic you want.',
+ logo: `${baseUrl}/favicon-32x32.png`,
+ icon: `${baseUrl}/favicon-32x32.png`,
+ language: 'en-us',
+ };
+}
diff --git a/lib/routes/daily/source.ts b/lib/routes/daily/source.ts
new file mode 100644
index 00000000000000..34bfec4e688e01
--- /dev/null
+++ b/lib/routes/daily/source.ts
@@ -0,0 +1,195 @@
+import { DataItem, Route } from '@/types';
+import { baseUrl, getBuildId, getData, getList } from './utils';
+import ofetch from '@/utils/ofetch';
+import cache from '@/utils/cache';
+import { config } from '@/config';
+
+interface Source {
+ id: string;
+ name: string;
+ handle: string;
+ image: string;
+ permalink: string;
+ description: string;
+ type: string;
+}
+
+const sourceFeedQuery = `
+query SourceFeed($source: ID!, $loggedIn: Boolean! = false, $first: Int, $after: String, $ranking: Ranking, $supportedTypes: [String!]) {
+ page: sourceFeed(
+ source: $source
+ first: $first
+ after: $after
+ ranking: $ranking
+ supportedTypes: $supportedTypes
+ ) {
+ ...FeedPostConnection
+ }
+}
+
+fragment FeedPostConnection on PostConnection {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ edges {
+ node {
+ ...FeedPost
+ pinnedAt
+ contentHtml
+ ...UserPost @include(if: $loggedIn)
+ }
+ }
+}
+
+fragment FeedPost on Post {
+ ...FeedPostInfo
+ sharedPost {
+ id
+ title
+ image
+ readTime
+ permalink
+ commentsPermalink
+ createdAt
+ type
+ tags
+ source {
+ id
+ handle
+ permalink
+ image
+ }
+ slug
+ }
+ trending
+ feedMeta
+ collectionSources {
+ handle
+ image
+ }
+ numCollectionSources
+ updatedAt
+ slug
+}
+
+fragment FeedPostInfo on Post {
+ id
+ title
+ image
+ readTime
+ permalink
+ commentsPermalink
+ createdAt
+ commented
+ bookmarked
+ views
+ numUpvotes
+ numComments
+ summary
+ bookmark {
+ remindAt
+ }
+ author {
+ id
+ name
+ image
+ username
+ permalink
+ }
+ type
+ tags
+ source {
+ id
+ handle
+ name
+ permalink
+ image
+ type
+ }
+ userState {
+ vote
+ flags {
+ feedbackDismiss
+ }
+ }
+ slug
+}
+
+fragment UserPost on Post {
+ read
+ upvoted
+ commented
+ bookmarked
+ downvoted
+}`;
+
+export const route: Route = {
+ path: '/source/:sourceId/:innerSharedContent?',
+ example: '/daily/source/hn',
+ parameters: {
+ sourceId: 'The source id',
+ innerSharedContent: {
+ description: 'Where to Fetch inner Shared Posts instead of original',
+ default: 'false',
+ options: [
+ { value: 'false', label: 'False' },
+ { value: 'true', label: 'True' },
+ ],
+ },
+ },
+ radar: [
+ {
+ source: ['app.daily.dev/sources/:sourceId'],
+ },
+ ],
+ name: 'Source Posts',
+ maintainers: ['TonyRL'],
+ handler,
+ url: 'app.daily.dev',
+};
+
+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 innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false;
+
+ const link = `${baseUrl}/sources/${sourceId}`;
+ const buildId = await getBuildId();
+
+ const userData = (await cache.tryGet(`daily:source:${sourceId}`, async () => {
+ const response = await ofetch(`${baseUrl}/_next/data/${buildId}/en/sources/${sourceId}.json`);
+ return response.pageProps.source;
+ })) as Source;
+
+ const items = await cache.tryGet(
+ `daily:source:${sourceId}:posts`,
+ async () => {
+ const edges = await getData({
+ query: sourceFeedQuery,
+ variables: {
+ source: sourceId,
+ supportedTypes: ['article', 'video:youtube', 'collection'],
+ period: 30,
+ first: limit,
+ after: '',
+ loggedIn: false,
+ },
+ });
+ return getList(edges, innerSharedContent, true);
+ },
+ config.cache.routeExpire,
+ false
+ );
+
+ return {
+ title: `${userData.name} posts on daily.dev`,
+ description: userData.description,
+ link,
+ item: items as DataItem[],
+ image: userData.image,
+ logo: userData.image,
+ icon: userData.image,
+ language: 'en-us',
+ };
+}
diff --git a/lib/routes/daily/squads.ts b/lib/routes/daily/squads.ts
new file mode 100644
index 00000000000000..1aabbd2d9346c9
--- /dev/null
+++ b/lib/routes/daily/squads.ts
@@ -0,0 +1,266 @@
+import { Route, ViewType } from '@/types';
+import { baseUrl, getData, getList, variables } from './utils.js';
+
+const sourceQuery = `
+query Source($handle: ID!) {
+ source(id: $handle) {
+ ...SquadBaseInfo
+ moderationPostCount
+ }
+ }
+ fragment SquadBaseInfo on Source {
+ ...SourceBaseInfo
+ referralUrl
+ createdAt
+ flags {
+ featured
+ totalPosts
+ totalViews
+ totalUpvotes
+ }
+ category {
+ id
+ title
+ slug
+ }
+ ...PrivilegedMembers
+ }
+ fragment SourceBaseInfo on Source {
+ id
+ active
+ handle
+ name
+ permalink
+ public
+ type
+ description
+ image
+ membersCount
+ currentMember {
+ ...CurrentMember
+ }
+ memberPostingRole
+ memberInviteRole
+ moderationRequired
+ }
+ fragment CurrentMember on SourceMember {
+ user {
+ id
+ }
+ permissions
+ role
+ referralToken
+ flags {
+ hideFeedPosts
+ collapsePinnedPosts
+ }
+ }
+ fragment PrivilegedMembers on Source {
+ privilegedMembers {
+ user {
+ id
+ name
+ image
+ permalink
+ username
+ bio
+ reputation
+ companies {
+ name
+ image
+ }
+ contentPreference {
+ status
+ }
+ }
+ role
+ }
+ }
+
+`;
+
+const query = `
+ query SourceFeed(
+ $source: ID!
+ $loggedIn: Boolean! = false
+ $first: Int
+ $after: String
+ $ranking: Ranking
+ $supportedTypes: [String!]
+ ) {
+ page: sourceFeed(
+ source: $source
+ first: $first
+ after: $after
+ ranking: $ranking
+ supportedTypes: $supportedTypes
+ ) {
+ ...FeedPostConnection
+ }
+ }
+
+ fragment FeedPostConnection on PostConnection {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ edges {
+ node {
+ ...FeedPost
+ pinnedAt contentHtml
+ ...UserPost @include(if: $loggedIn)
+ }
+ }
+ }
+
+ fragment FeedPost on Post {
+ ...FeedPostInfo
+ sharedPost {
+ id
+ title
+ image
+ readTime
+ permalink
+ commentsPermalink
+ createdAt
+ type
+ tags
+ source {
+ id
+ handle
+ permalink
+ image
+ }
+ slug
+ clickbaitTitleDetected
+ }
+ trending
+ feedMeta
+ collectionSources {
+ handle
+ image
+ }
+ numCollectionSources
+ updatedAt
+ slug
+ }
+
+ fragment FeedPostInfo on Post {
+ id
+ title
+ image
+ readTime
+ permalink
+ commentsPermalink
+ createdAt
+ commented
+ bookmarked
+ views
+ numUpvotes
+ numComments
+ summary
+ bookmark {
+ remindAt
+ }
+ author {
+ id
+ name
+ image
+ username
+ permalink
+ }
+ type
+ tags
+ source {
+ id
+ handle
+ name
+ permalink
+ image
+ type
+ }
+ userState {
+ vote
+ flags {
+ feedbackDismiss
+ }
+ }
+ slug
+ clickbaitTitleDetected
+ }
+
+
+
+ fragment UserPost on Post {
+ read
+ upvoted
+ commented
+ bookmarked
+ downvoted
+ }
+`;
+
+export const route: Route = {
+ path: '/squads/:squads/:innerSharedContent?',
+ example: '/daily/squads/watercooler',
+ view: ViewType.Articles,
+ parameters: {
+ innerSharedContent: {
+ description: 'Where to Fetch inner Shared Posts instead of original',
+ default: 'false',
+ options: [
+ { value: 'false', label: 'False' },
+ { value: 'true', label: 'True' },
+ ],
+ },
+ },
+ radar: [
+ {
+ source: ['app.daily.dev/squads/:squads'],
+ },
+ ],
+ name: 'Squads',
+ maintainers: ['Rjnishant530'],
+ handler,
+ url: 'app.daily.dev/squads/discover',
+};
+
+async function handler(ctx) {
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
+ const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false;
+ const squads = ctx.req.param('squads');
+
+ const link = `${baseUrl}/squads/${squads}`;
+
+ const { id, description, name } = await getData(
+ {
+ query: sourceQuery,
+ variables: {
+ handle: squads,
+ },
+ },
+ true
+ );
+
+ const data = await getData({
+ query,
+ variables: {
+ ...variables,
+ source: id,
+ ranking: 'TIME',
+ supportedTypes: ['article', 'share', 'freeform', 'video:youtube', 'collection', 'welcome'],
+ first: limit,
+ },
+ });
+ const items = getList(data, innerSharedContent, true);
+
+ return {
+ title: `${name} - daily.dev`,
+ link,
+ item: items,
+ description,
+ logo: `${baseUrl}/favicon-32x32.png`,
+ icon: `${baseUrl}/favicon-32x32.png`,
+ language: 'en-us',
+ };
+}
diff --git a/lib/routes/daily/upvoted.ts b/lib/routes/daily/upvoted.ts
index 68a2a19d6cc784..0633880f6e4f2e 100644
--- a/lib/routes/daily/upvoted.ts
+++ b/lib/routes/daily/upvoted.ts
@@ -1,92 +1,189 @@
-import { Route } from '@/types';
-import { getData, getList, getRedirectedLink } from './utils.js';
-
-const variables = {
- period: 7,
- first: 15,
-};
+import { Route, ViewType } from '@/types';
+import { baseUrl, getData, getList, variables } from './utils.js';
const query = `
- query MostUpvotedFeed(
+ query MostUpvotedFeed(
+ $loggedIn: Boolean! = false
$first: Int
+ $after: String
$period: Int
- $supportedTypes: [String!] = ["article","share","freeform"]
+ $supportedTypes: [String!] = ["article","share","freeform","video:youtube","collection"]
+ $source: ID
+ $tag: String
) {
- page: mostUpvotedFeed(first: $first, period: $period, supportedTypes: $supportedTypes) {
+ page: mostUpvotedFeed(first: $first, after: $after, period: $period, supportedTypes: $supportedTypes, source: $source, tag: $tag) {
...FeedPostConnection
}
}
-
+
fragment FeedPostConnection on PostConnection {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
edges {
node {
...FeedPost
+ contentHtml
+ ...UserPost @include(if: $loggedIn)
}
}
}
-
+
fragment FeedPost on Post {
- ...SharedPostInfo
+ ...FeedPostInfo
+ sharedPost {
+ id
+ title
+ image
+ readTime
+ permalink
+ commentsPermalink
+ createdAt
+ type
+ tags
+ source {
+ id
+ handle
+ permalink
+ image
+ }
+ slug
+ clickbaitTitleDetected
+ }
+ trending
+ feedMeta
+ collectionSources {
+ handle
+ image
+ }
+ numCollectionSources
+ updatedAt
+ slug
}
-
- fragment SharedPostInfo on Post {
+
+ fragment FeedPostInfo on Post {
id
title
image
readTime
permalink
- summary
+ commentsPermalink
createdAt
+ commented
+ bookmarked
+ views
numUpvotes
numComments
+ summary
+ bookmark {
+ remindAt
+ }
author {
- ...UserShortInfo
+ id
+ name
+ image
+ username
+ permalink
}
+ type
tags
+ source {
+ id
+ handle
+ name
+ permalink
+ image
+ type
+ }
+ userState {
+ vote
+ flags {
+ feedbackDismiss
+ }
+ }
+ slug
+ clickbaitTitleDetected
}
- fragment UserShortInfo on User {
- id
- name
- image
- permalink
- username
- bio
+
+
+ fragment UserPost on Post {
+ read
+ upvoted
+ commented
+ bookmarked
+ downvoted
}
`;
-const graphqlQuery = {
- query,
- variables,
-};
-
export const route: Route = {
- path: '/upvoted',
- example: '/daily/upvoted',
+ path: '/upvoted/:period?/:innerSharedContent?/:dateSort?',
+ example: '/daily/upvoted/7',
+ view: ViewType.Articles,
radar: [
{
- source: ['daily.dev/popular'],
+ source: ['app.daily.dev/upvoted'],
},
],
+ parameters: {
+ innerSharedContent: {
+ description: 'Where to Fetch inner Shared Posts instead of original',
+ default: 'false',
+ options: [
+ { value: 'false', label: 'False' },
+ { value: 'true', label: 'True' },
+ ],
+ },
+ dateSort: {
+ description: 'Sort posts by publication date instead of popularity',
+ default: 'true',
+ options: [
+ { value: 'false', label: 'False' },
+ { value: 'true', label: 'True' },
+ ],
+ },
+ period: {
+ description: 'Period of Lookup',
+ default: '7',
+ options: [
+ { value: '7', label: 'Last Week' },
+ { value: '30', label: 'Last Month' },
+ { value: '365', label: 'Last Year' },
+ ],
+ },
+ },
name: 'Most upvoted',
maintainers: ['Rjnishant530'],
handler,
- url: 'daily.dev/popular',
+ url: 'app.daily.dev/upvoted',
};
-async function handler() {
- const baseUrl = 'https://app.daily.dev/upvoted';
- const data = await getData(graphqlQuery);
- const list = getList(data);
- const items = await getRedirectedLink(list);
+async function handler(ctx) {
+ const link = `${baseUrl}/posts/upvoted`;
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 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 data = await getData({
+ query,
+ variables: {
+ ...variables,
+ period,
+ first: limit,
+ },
+ });
+ const items = getList(data, innerSharedContent, dateSort);
+
return {
- title: 'Most Upvoted',
- link: baseUrl,
+ title: 'Most upvoted posts for developers | daily.dev',
+ link,
item: items,
- description: 'Most Upvoted Posts on Daily.dev',
- logo: 'https://app.daily.dev/favicon-32x32.png',
- icon: 'https://app.daily.dev/favicon-32x32.png',
+ description: 'Find the most upvoted developer posts on daily.dev. Explore top-rated content in coding, tutorials, and tech news from the largest developer network in the world.',
+ logo: `${baseUrl}/favicon-32x32.png`,
+ icon: `${baseUrl}/favicon-32x32.png`,
language: 'en-us',
};
}
diff --git a/lib/routes/daily/user.ts b/lib/routes/daily/user.ts
index 7abaf15a029cb8..579ea0bcbb8d1d 100644
--- a/lib/routes/daily/user.ts
+++ b/lib/routes/daily/user.ts
@@ -1,13 +1,8 @@
-import { Route } from '@/types';
-import { baseUrl, getBuildId, getData } from './utils';
+import { DataItem, Route } from '@/types';
+import { baseUrl, getBuildId, getData, getList } from './utils';
import ofetch from '@/utils/ofetch';
import cache from '@/utils/cache';
import { config } from '@/config';
-import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
-import path from 'path';
-import { getCurrentPath } from '@/utils/helpers';
-const __dirname = getCurrentPath(import.meta.url);
const userPostQuery = `
query AuthorFeed(
@@ -155,35 +150,43 @@ const userPostQuery = `
downvoted
}`;
-const render = (data) => art(path.join(__dirname, 'templates/posts.art'), data);
-
export const route: Route = {
- path: '/user/:userId',
+ path: '/user/:userId/:innerSharedContent?',
example: '/daily/user/kramer',
radar: [
{
- source: ['daily.dev/:userId/posts', 'daily.dev/:userId'],
+ source: ['app.daily.dev/:userId/posts', 'app.daily.dev/:userId'],
},
],
+ parameters: {
+ innerSharedContent: {
+ description: 'Where to Fetch inner Shared Posts instead of original',
+ default: 'false',
+ options: [
+ { value: 'false', label: 'False' },
+ { value: 'true', label: 'True' },
+ ],
+ },
+ },
name: 'User Posts',
maintainers: ['TonyRL'],
handler,
- url: 'daily.dev',
+ url: 'app.daily.dev',
};
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 innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false;
const buildId = await getBuildId();
const userData = await cache.tryGet(`daily:user:${userId}`, async () => {
- const resposne = await ofetch(`${baseUrl}/_next/data/${buildId}/en/${userId}.json`, {
+ const response = await ofetch(`${baseUrl}/_next/data/${buildId}/en/${userId}.json`, {
query: {
userId,
},
});
- return resposne.pageProps;
+ return response.pageProps;
});
const user = (userData as any).user;
@@ -198,17 +201,7 @@ async function handler(ctx) {
loggedIn: false,
},
});
- return edges.map(({ node }) => ({
- title: node.title,
- description: render({
- image: node.image,
- content: node.contentHtml?.replaceAll('\n', '
') ?? node.summary,
- }),
- link: node.permalink,
- author: node.author?.name,
- category: node.tags,
- pubDate: parseDate(node.createdAt),
- }));
+ return getList(edges, innerSharedContent, true);
},
config.cache.routeExpire,
false
@@ -218,7 +211,7 @@ async function handler(ctx) {
title: `${user.name} | daily.dev`,
description: user.bio,
link: `${baseUrl}/${userId}/posts`,
- item: items,
+ item: items as DataItem[],
image: user.image,
logo: user.image,
icon: user.image,
diff --git a/lib/routes/daily/utils.ts b/lib/routes/daily/utils.ts
index 8f203c744d15a3..eb559762dcb0a1 100644
--- a/lib/routes/daily/utils.ts
+++ b/lib/routes/daily/utils.ts
@@ -2,10 +2,19 @@ import { parseDate } from '@/utils/parse-date';
import ofetch from '@/utils/ofetch';
import cache from '@/utils/cache';
import { config } from '@/config';
+import { art } from '@/utils/render';
+import path from 'node:path';
+import { getCurrentPath } from '@/utils/helpers';
+import { DataItem } from '@/types';
+const __dirname = getCurrentPath(import.meta.url);
-const baseUrl = 'https://app.daily.dev';
-
-const getBuildId = () =>
+export const baseUrl = 'https://app.daily.dev';
+const gqlUrl = `https://api.daily.dev/graphql`;
+export const variables = {
+ version: 54,
+ loggedIn: false,
+};
+export const getBuildId = () =>
cache.tryGet(
'daily:buildId',
async () => {
@@ -17,40 +26,42 @@ const getBuildId = () =>
false
);
-const getData = async (graphqlQuery) => {
- const response = await ofetch(`${baseUrl}/api/graphql`, {
+export const getData = async (graphqlQuery, source = false) => {
+ const response = await ofetch(gqlUrl, {
method: 'POST',
body: graphqlQuery,
});
- return response.data.page.edges;
+ return source ? response.data.source : response.data.page.edges;
};
-const getList = (data) =>
- data.map((value) => {
- const { id, title, image, permalink, summary, createdAt, numUpvotes, author, tags, numComments } = value.node;
- const pubDate = parseDate(createdAt);
+const render = (data) => art(path.join(__dirname, 'templates/posts.art'), data);
+
+export const getList = (edges, innerSharedContent: boolean, dateSort: boolean) =>
+ edges.map(({ node }) => {
+ let link: string;
+ let title: string;
+ if (innerSharedContent && node.type === 'share') {
+ link = node.sharedPost.permalink;
+ title = node.sharedPost.title;
+ } else {
+ link = node.commentsPermalink ?? node.permalink;
+ title = node.title;
+ }
+
return {
- id,
+ id: node.id,
title,
- link: permalink,
- description: summary,
- author: author?.name,
- itunes_item_image: image,
- pubDate,
- upvotes: numUpvotes,
- comments: numComments,
- category: tags,
- };
+ link,
+ guid: node.permalink,
+ description: render({
+ image: node.image,
+ content: node.contentHtml?.replaceAll('\n', '
') ?? node.summary,
+ }),
+ author: node.author?.name,
+ itunes_item_image: node.image,
+ pubDate: dateSort ? parseDate(node.createdAt) : '',
+ upvotes: node.numUpvotes,
+ comments: node.numComments,
+ category: node.tags,
+ } as DataItem;
});
-
-const getRedirectedLink = (data) =>
- Promise.all(
- data.map((v) =>
- cache.tryGet(v.link, async () => {
- const resp = await ofetch.raw(v.link);
- return { ...v, link: resp.headers.get('location') };
- })
- )
- );
-
-export { baseUrl, getBuildId, getData, getList, getRedirectedLink };
diff --git a/lib/routes/damai/activity.ts b/lib/routes/damai/activity.ts
index ff26f7b209c683..bcf0962bcf926f 100644
--- a/lib/routes/damai/activity.ts
+++ b/lib/routes/damai/activity.ts
@@ -15,13 +15,13 @@ export const route: Route = {
features: {
requireConfig: false,
requirePuppeteer: false,
- antiCrawler: false,
+ antiCrawler: true,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '票务更新',
- maintainers: ['hoilc'],
+ maintainers: ['hoilc', 'Konano'],
handler,
description: `城市、分类名、子分类名,请参见[大麦网搜索页面](https://search.damai.cn/search.htm)`,
};
@@ -55,6 +55,7 @@ async function handler(ctx) {
return {
title: `大麦网票务 - ${city || '全国'} - ${category || '全部分类'}${subcategory ? ' - ' + subcategory : ''}${keyword ? ' - ' + keyword : ''}`,
link: 'https://search.damai.cn/search.htm',
+ allowEmpty: true,
item: list.map((item) => ({
title: item.nameNoHtml,
author: item.actors ? load(item.actors, null, false).text() : '大麦网',
diff --git a/lib/routes/damai/namespace.ts b/lib/routes/damai/namespace.ts
index 72327dc8ae10d8..6ef673c9026deb 100644
--- a/lib/routes/damai/namespace.ts
+++ b/lib/routes/damai/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '大麦网',
url: 'search.damai.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/dangdang/namespace.ts b/lib/routes/dangdang/namespace.ts
new file mode 100644
index 00000000000000..13a9b5270676f9
--- /dev/null
+++ b/lib/routes/dangdang/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '当当开放平台',
+ url: 'open.dangdang.com',
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/dangdang/notice.ts b/lib/routes/dangdang/notice.ts
new file mode 100644
index 00000000000000..d21414facdbf29
--- /dev/null
+++ b/lib/routes/dangdang/notice.ts
@@ -0,0 +1,69 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+
+const typeMap = {
+ 0: '全部',
+ 1: '其他',
+ 2: '规则变更',
+};
+
+/**
+ *
+ * @param ctx {import('koa').Context}
+ */
+export const route: Route = {
+ path: '/notice/:type?',
+ categories: ['programming'],
+ example: '/dangdang/notice/1',
+ parameters: { type: '公告分类,默认为全部' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ name: '公告',
+ maintainers: ['353325487'],
+ handler,
+ description: `| 类型 | type |
+| -------- | ---- |
+| 全部 | 0 |
+| 其他 | 1 |
+| 规则变更 | 2 |`,
+};
+
+async function handler(ctx) {
+ const type = ctx.req.param('type');
+ const url = `https://open.dangdang.com/op-api/developer-platform/document/menu/list?categoryId=3&type=${type > 0 ? typeMap[type] : ''}`;
+ const response = await got({ method: 'get', url });
+
+ const list = response.data.data.documentMenu.map((item) => ({
+ title: item.title,
+ description: item.type,
+ documentId: item.documentId,
+ source: `https://open.dangdang.com/op-api/developer-platform/document/info/get?document_id=${item.documentId}`,
+ link: `https://open.dangdang.com/home/notice/message/1/${item.documentId}`,
+ pubDate: timezone(parseDate(item.modifyTime), +8),
+ }));
+
+ const result = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.source, async () => {
+ const itemResponse = await got(item.source);
+ item.description = itemResponse.data.data.documentContentList[0].content;
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: `当当开放平台 - ${typeMap[type] || typeMap[0]}`,
+ link: `https://open.dangdang.com/home/notice/message/1`,
+ item: result,
+ };
+}
diff --git a/lib/routes/daoxuan/namespace.ts b/lib/routes/daoxuan/namespace.ts
new file mode 100644
index 00000000000000..19ae3d23a55788
--- /dev/null
+++ b/lib/routes/daoxuan/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '道宣的窝',
+ url: 'daoxuan.cc',
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/daoxuan/rss.ts b/lib/routes/daoxuan/rss.ts
new file mode 100644
index 00000000000000..46b847728d86e2
--- /dev/null
+++ b/lib/routes/daoxuan/rss.ts
@@ -0,0 +1,43 @@
+import { Route } from '@/types';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+
+export const route: Route = {
+ path: '/',
+ categories: ['blog'],
+ example: '/daoxuan',
+ radar: [
+ {
+ source: ['daoxuan.cc/'],
+ },
+ ],
+ name: '推荐阅读文章',
+ maintainers: ['dx2331lxz'],
+ url: 'daoxuan.cc/',
+ handler,
+};
+
+async function handler() {
+ const url = 'https://daoxuan.cc/';
+ const response = await got({ method: 'get', url });
+ const $ = load(response.data);
+ const items = $('div.recent-post-item')
+ .toArray()
+ .map((item) => {
+ item = $(item);
+ const a = item.find('a.article-title').first();
+ const timeElement = item.find('time').first();
+ return {
+ title: a.attr('title'),
+ link: `https://daoxuan.cc${a.attr('href')}`,
+ pubDate: parseDate(timeElement.attr('datetime')),
+ description: a.attr('title'),
+ };
+ });
+ return {
+ title: '道宣的窝',
+ link: url,
+ item: items,
+ };
+}
diff --git a/lib/routes/dapenti/namespace.ts b/lib/routes/dapenti/namespace.ts
index e3fe6f0a783bb2..6c3b0eba14c33c 100644
--- a/lib/routes/dapenti/namespace.ts
+++ b/lib/routes/dapenti/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '喷嚏',
url: 'dapenti.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/darwinawards/index.ts b/lib/routes/darwinawards/index.ts
index 3e8b5d813526cd..ee5567d7db7f32 100644
--- a/lib/routes/darwinawards/index.ts
+++ b/lib/routes/darwinawards/index.ts
@@ -4,18 +4,17 @@ import got from '@/utils/got';
import { load } from 'cheerio';
export const route: Route = {
- path: ['/all', '/'],
+ name: 'Award Winners',
+ example: '/darwinawards',
+ path: '/',
radar: [
{
source: ['darwinawards.com/darwin', 'darwinawards.com/'],
- target: '',
},
],
- name: 'Unknown',
maintainers: ['zoenglinghou', 'nczitzk'],
handler,
url: 'darwinawards.com/darwin',
- url: 'darwinawards.com/darwin',
};
async function handler() {
diff --git a/lib/routes/darwinawards/namespace.ts b/lib/routes/darwinawards/namespace.ts
index 77610ba63ae3b0..e1895ace98e040 100644
--- a/lib/routes/darwinawards/namespace.ts
+++ b/lib/routes/darwinawards/namespace.ts
@@ -3,4 +3,6 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Darwin Awards',
url: 'darwinawards.com',
+ categories: ['other'],
+ lang: 'en',
};
diff --git a/lib/routes/dataguidance/index.ts b/lib/routes/dataguidance/index.ts
new file mode 100644
index 00000000000000..d44b4b4c5bb61d
--- /dev/null
+++ b/lib/routes/dataguidance/index.ts
@@ -0,0 +1,55 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch';
+
+import { parseDate } from '@/utils/parse-date';
+
+export const route: Route = {
+ name: 'News',
+ example: '/dataguidance/news',
+ path: '/news',
+ radar: [
+ {
+ source: ['www.dataguidance.com/info'],
+ },
+ ],
+ maintainers: ['harveyqiu'],
+ handler,
+ url: 'https://www.dataguidance.com/info?article_type=news_post',
+};
+
+async function handler() {
+ const rootUrl = 'https://www.dataguidance.com';
+ const url = 'https://www.dataguidance.com/api/v1/kb/content/articles?news_types=510&news_types=511&news_types=512&news_types=513&order=DESC_publishedOn&limit=25&article_types=news_post';
+
+ const response = await ofetch(url);
+
+ const data = response.data;
+
+ let items = data.map((item) => ({
+ title: item.title.en,
+ link: `${rootUrl}${item.url}`,
+ url: item.url,
+ pubDate: parseDate(item.publishedOn),
+ }));
+ const baseUrl = 'https://www.dataguidance.com/api/v1/kb/content/articles/by_path?path=';
+ items = await Promise.all(
+ items.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const detailUrl = `${baseUrl}${item.url}`;
+
+ const detailResponse = await ofetch(detailUrl);
+
+ item.description = detailResponse.contentBody?.html.en.replaceAll('\n', '
');
+ delete item.url;
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: 'Data Guidance News',
+ link: 'https://www.dataguidance.com/info?article_type=news_post',
+ item: items,
+ };
+}
diff --git a/lib/routes/dataguidance/namespace.ts b/lib/routes/dataguidance/namespace.ts
new file mode 100644
index 00000000000000..14494e43fcb171
--- /dev/null
+++ b/lib/routes/dataguidance/namespace.ts
@@ -0,0 +1,8 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'DataGuidance',
+ url: 'dataguidance.com',
+ categories: ['other'],
+ lang: 'en',
+};
diff --git a/lib/routes/dayanzai/index.ts b/lib/routes/dayanzai/index.ts
index 7bf7d675f6c213..7fecbc7b42d0d1 100644
--- a/lib/routes/dayanzai/index.ts
+++ b/lib/routes/dayanzai/index.ts
@@ -30,8 +30,8 @@ export const route: Route = {
maintainers: [],
handler,
description: `| 微软应用 | 安卓应用 | 教程资源 | 其他资源 |
- | -------- | -------- | -------- | -------- |
- | windows | android | tutorial | other |`,
+| -------- | -------- | -------- | -------- |
+| windows | android | tutorial | other |`,
};
async function handler(ctx) {
diff --git a/lib/routes/dayanzai/namespace.ts b/lib/routes/dayanzai/namespace.ts
index 3a62c54e3b1836..d9c15d2d0da0c6 100644
--- a/lib/routes/dayanzai/namespace.ts
+++ b/lib/routes/dayanzai/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '大眼仔旭',
url: 'dayanzai.me',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/dbaplus/namespace.ts b/lib/routes/dbaplus/namespace.ts
index 5de237b67c7e95..0d48834010b8a0 100644
--- a/lib/routes/dbaplus/namespace.ts
+++ b/lib/routes/dbaplus/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'dbaplus社群',
url: 'dbaplus.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/dblp/namespace.ts b/lib/routes/dblp/namespace.ts
index 9cd978d440fa05..4a07bf01a91494 100644
--- a/lib/routes/dblp/namespace.ts
+++ b/lib/routes/dblp/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'DBLP',
url: 'dblp.org',
+ lang: 'en',
};
diff --git a/lib/routes/dblp/publication.ts b/lib/routes/dblp/publication.ts
index 90b1f99e0a9b57..e430c9e78794e7 100644
--- a/lib/routes/dblp/publication.ts
+++ b/lib/routes/dblp/publication.ts
@@ -1,6 +1,6 @@
import { Route } from '@/types';
// 导入所需模组
-import got from '@/utils/got'; // 自订的 got
+import ofetch from '@/utils/ofetch';
// import { parseDate } from '@/utils/parse-date';
export const route: Route = {
@@ -35,10 +35,8 @@ async function handler(ctx) {
result: {
hits: { hit: data },
},
- } = await got({
- method: 'get',
- url: 'https://dblp.org/search/publ/api',
- searchParams: {
+ } = await ofetch('https://dblp.org/search/publ/api', {
+ query: {
q: field,
format: 'json',
h: 10,
@@ -46,7 +44,7 @@ async function handler(ctx) {
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
- }).json();
+ });
// console.log(data);
diff --git a/lib/routes/dcard/namespace.ts b/lib/routes/dcard/namespace.ts
index 8e7fbc8a1eefdd..b56302ac7e9d96 100644
--- a/lib/routes/dcard/namespace.ts
+++ b/lib/routes/dcard/namespace.ts
@@ -3,7 +3,8 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Dcard',
url: 'www.dcard.tw',
- description: `:::warning
+ description: `::: warning
僅能透過台灣 IP 抓取。
:::`,
+ lang: 'zh-TW',
};
diff --git a/lib/routes/dcfever/namespace.ts b/lib/routes/dcfever/namespace.ts
index f14a921ffd9c43..b97d35d0b6c290 100644
--- a/lib/routes/dcfever/namespace.ts
+++ b/lib/routes/dcfever/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'DCFever',
url: 'dcfever.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/dcfever/news.ts b/lib/routes/dcfever/news.ts
index e9364c9cf40d10..36d195609d6053 100644
--- a/lib/routes/dcfever/news.ts
+++ b/lib/routes/dcfever/news.ts
@@ -5,7 +5,7 @@ import { baseUrl, parseItem } from './utils';
export const route: Route = {
path: '/news/:type?',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/dcfever/news',
parameters: { type: '分類,預設為所有新聞' },
name: '新聞中心',
@@ -18,8 +18,8 @@ export const route: Route = {
},
],
description: `| 所有新聞 | 攝影器材 | 手機通訊 | 汽車熱話 | 攝影文化 | 影片攝錄 | 測試報告 | 生活科技 | 攝影技巧 |
- | -------- | -------- | -------- | -------- | ----------- | ----------- | -------- | -------- | --------- |
- | | camera | mobile | auto | photography | videography | reviews | gadget | technique |`,
+| -------- | -------- | -------- | -------- | ----------- | ----------- | -------- | -------- | --------- |
+| | camera | mobile | auto | photography | videography | reviews | gadget | technique |`,
};
async function handler(ctx) {
diff --git a/lib/routes/dcfever/reviews.ts b/lib/routes/dcfever/reviews.ts
index f7ba620d79ab23..f5f393a012594d 100644
--- a/lib/routes/dcfever/reviews.ts
+++ b/lib/routes/dcfever/reviews.ts
@@ -5,7 +5,7 @@ import { baseUrl, parseItem } from './utils';
export const route: Route = {
path: '/reviews/:type?',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/dcfever/reviews/cameras',
parameters: { type: '分類,預設為 `cameras`' },
radar: [
@@ -18,8 +18,8 @@ export const route: Route = {
maintainers: ['TonyRL'],
handler,
description: `| 相機及鏡頭 | 手機平板 | 試車報告 |
- | ---------- | -------- | -------- |
- | cameras | phones | cars |`,
+| ---------- | -------- | -------- |
+| cameras | phones | cars |`,
};
async function handler(ctx) {
diff --git a/lib/routes/dcfever/trading-search.ts b/lib/routes/dcfever/trading-search.ts
index b5d85a60a4ba6f..f812a301039186 100644
--- a/lib/routes/dcfever/trading-search.ts
+++ b/lib/routes/dcfever/trading-search.ts
@@ -6,7 +6,7 @@ import { baseUrl, parseTradeItem } from './utils';
export const route: Route = {
path: '/trading/search/:keyword/:mainCat?',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/dcfever/trading/search/Sony',
parameters: { keyword: '關鍵字', mainCat: '主要分類 ID,見上表' },
name: '二手市集 - 物品搜尋',
diff --git a/lib/routes/dcfever/trading.ts b/lib/routes/dcfever/trading.ts
index 6641f7f4e8bb80..31396540b4e1df 100644
--- a/lib/routes/dcfever/trading.ts
+++ b/lib/routes/dcfever/trading.ts
@@ -6,7 +6,7 @@ import { baseUrl, parseTradeItem } from './utils';
export const route: Route = {
path: '/trading/:id',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/dcfever/trading/1',
parameters: { id: '分類 ID,見下表' },
name: '二手市集',
@@ -14,9 +14,9 @@ export const route: Route = {
handler,
description: `[所有物品分類](https://www.dcfever.com/trading/index.php#all_cats)
- | 攝影產品 | 電腦 | 手機通訊 | 影音產品 | 遊戲機、模型 | 電器傢俱 | 潮流服飾 | 手錶 | 單車及運動 | 其它 |
- | -------- | ---- | -------- | -------- | ------------ | -------- | -------- | ---- | ---------- | ---- |
- | 1 | 2 | 3 | 44 | 43 | 104 | 45 | 99 | 109 | 4 |`,
+| 攝影產品 | 電腦 | 手機通訊 | 影音產品 | 遊戲機、模型 | 電器傢俱 | 潮流服飾 | 手錶 | 單車及運動 | 其它 |
+| -------- | ---- | -------- | -------- | ------------ | -------- | -------- | ---- | ---------- | ---- |
+| 1 | 2 | 3 | 44 | 43 | 104 | 45 | 99 | 109 | 4 |`,
};
async function handler(ctx) {
@@ -29,16 +29,14 @@ async function handler(ctx) {
const response = await ofetch(link.href);
const $ = load(response);
- const list = $('.item_list li a')
+ const list = $('.item_grid_wrap div a')
.toArray()
- .filter((item) => $(item).attr('href') !== '/documents/advertising.php')
.map((item) => {
item = $(item);
- item.find('.optional').remove();
return {
- title: item.find('.trade_title').text(),
+ title: item.find('.lazyloadx').attr('alt'),
link: new URL(item.attr('href'), link.href).href,
- author: item.find('.trade_info').text(),
+ author: item.find('.trade_info div span').eq(1).text(),
};
});
diff --git a/lib/routes/dcfever/utils.ts b/lib/routes/dcfever/utils.ts
index 8f8527bbf96883..c2223147716658 100644
--- a/lib/routes/dcfever/utils.ts
+++ b/lib/routes/dcfever/utils.ts
@@ -63,11 +63,20 @@ const parseTradeItem = (item) =>
const response = await ofetch(item.link);
const $ = load(response);
- $('.selector_text').remove();
- $('.selector_image_div').each((_, div) => {
+ const photoSelector = $('#trading_item_section .description')
+ .contents()
+ .filter((_, e) => e.type === 'comment')
+ .toArray()
+ .map((e) => e.data)
+ .join('');
+
+ const $photo = load(photoSelector, null, false);
+
+ $photo('.selector_text').remove();
+ $photo('.selector_image_div').each((_, div) => {
delete div.attribs.onclick;
});
- $('.desktop_photo_selector img').each((_, img) => {
+ $photo('.desktop_photo_selector img').each((_, img) => {
if (img.attribs.src.endsWith('_sqt.jpg')) {
img.attribs.src = img.attribs.src.replace('_sqt.jpg', '.jpg');
}
@@ -76,7 +85,7 @@ const parseTradeItem = (item) =>
item.description = art(path.join(__dirname, 'templates/trading.art'), {
info: $('.info_col'),
description: $('.description_text').html(),
- photo: $('.desktop_photo_selector').html(),
+ photo: $photo('.desktop_photo_selector').html(),
});
return item;
diff --git a/lib/routes/ddosi/namespace.ts b/lib/routes/ddosi/namespace.ts
index b7215ed8e661db..3eb85fb5ddbe74 100644
--- a/lib/routes/ddosi/namespace.ts
+++ b/lib/routes/ddosi/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '雨苁博客',
url: 'ddosi.org',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/deadbydaylight/index.ts b/lib/routes/deadbydaylight/index.ts
new file mode 100644
index 00000000000000..e32a7bc507e5c2
--- /dev/null
+++ b/lib/routes/deadbydaylight/index.ts
@@ -0,0 +1,69 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch';
+import { parseDate } from '@/utils/parse-date';
+import MarkdownIt from 'markdown-it';
+const md = MarkdownIt({
+ html: true,
+ linkify: true,
+});
+
+const baseUrl = 'https://deadbydaylight.com';
+
+export const route: Route = {
+ path: '/blog',
+ categories: ['game'],
+ example: '/deadbydaylight/blog',
+ parameters: {},
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['deadbydaylight.com/news'],
+ target: '/news',
+ },
+ ],
+ name: 'Latest News',
+ maintainers: ['NeverBehave'],
+ handler,
+};
+
+async function handler() {
+ const data = await ofetch(`${baseUrl}/page-data/news/page-data.json`);
+
+ const articleMeta = data.result.pageContext.postsData.articles.edges;
+ // { 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;
+ const slug = content.slug;
+ const dataUrl = `${baseUrl}/page-data/news/${slug}/page-data.json`;
+
+ return cache.tryGet(dataUrl, async () => {
+ const articleData = await ofetch(dataUrl);
+ const pageData = articleData.result.data.pageData;
+
+ return {
+ title: pageData.title,
+ link: `${baseUrl}${articleData.path}`,
+ description: md.render(pageData.content),
+ pubDate: parseDate(pageData.published_at),
+ category: pageData.article_category.name,
+ };
+ });
+ })
+ );
+
+ return {
+ title: 'Latest News',
+ link: 'https://deadbydaylight.com/news',
+ item: items,
+ };
+}
diff --git a/lib/routes/deadbydaylight/namespace.ts b/lib/routes/deadbydaylight/namespace.ts
new file mode 100644
index 00000000000000..7333371e3183d3
--- /dev/null
+++ b/lib/routes/deadbydaylight/namespace.ts
@@ -0,0 +1,13 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'DeadbyDaylight',
+ url: 'deadbydaylight.com',
+ description: `
+ DeadbyDaylight Official
+ `,
+ zh: {
+ name: '黎明杀机',
+ },
+ lang: 'en',
+};
diff --git a/lib/routes/deadline/namespace.ts b/lib/routes/deadline/namespace.ts
index 59c3c11569ed9b..1162e861451a8a 100644
--- a/lib/routes/deadline/namespace.ts
+++ b/lib/routes/deadline/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Deadline',
url: 'deadline.com',
+ lang: 'en',
};
diff --git a/lib/routes/dealstreetasia/home.ts b/lib/routes/dealstreetasia/home.ts
new file mode 100644
index 00000000000000..f48b56eca0306b
--- /dev/null
+++ b/lib/routes/dealstreetasia/home.ts
@@ -0,0 +1,72 @@
+import { Route } from '@/types';
+// import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch'; // Unified request library used
+import { load } from 'cheerio'; // An HTML parser with an API similar to jQuery
+// import puppeteer from '@/utils/puppeteer';
+// import { parseDate } from '@/utils/parse-date';
+
+export const route: Route = {
+ path: '/home',
+ categories: ['traditional-media'],
+ example: '/dealstreetasia/home',
+ // parameters: { section: 'target section' },
+ radar: [
+ {
+ source: ['dealstreetasia.com/'],
+ },
+ ],
+ name: 'Home',
+ maintainers: ['jack2game'],
+ handler,
+ url: 'dealstreetasia.com/',
+};
+
+async function handler() {
+ // const section = ctx.req.param('section');
+ const items = await fetchPage();
+
+ return items;
+}
+
+async function fetchPage() {
+ const baseUrl = 'https://dealstreetasia.com'; // Define base URL
+
+ const response = await ofetch(`${baseUrl}/`);
+ const $ = load(response);
+
+ const jsonData = JSON.parse($('#__NEXT_DATA__').text());
+ // const headingText = jsonData.props.pageProps.sectionData.name;
+
+ const pageProps = jsonData.props.pageProps;
+ const list = [
+ ...pageProps.topStories,
+ ...pageProps.privateEquity,
+ ...pageProps.ventureCapital,
+ ...pageProps.unicorns,
+ ...pageProps.interviews,
+ ...pageProps.deals,
+ ...pageProps.analysis,
+ ...pageProps.ipos,
+ ...pageProps.opinion,
+ ...pageProps.policyAndRegulations,
+ ...pageProps.people,
+ ...pageProps.earningsAndResults,
+ ...pageProps.theLpView,
+ ...pageProps.dvNewsletters,
+ ...pageProps.reports,
+ ].map((item) => ({
+ title: item.post_title || item.title || 'No Title',
+ 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
+ image: item.image_url ? item.image_url.replace(/\?.*$/, '') : '', // Remove query parameters if image_url exists
+ }));
+
+ return {
+ title: 'Deal Street Asia',
+ language: 'en',
+ item: list,
+ link: 'https://dealstreetasia.com/',
+ };
+}
diff --git a/lib/routes/dealstreetasia/namespace.ts b/lib/routes/dealstreetasia/namespace.ts
new file mode 100644
index 00000000000000..8395378e8187c2
--- /dev/null
+++ b/lib/routes/dealstreetasia/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'DealStreetAsia',
+ url: 'dealstreetasia.com',
+ lang: 'en',
+};
diff --git a/lib/routes/dealstreetasia/section.ts b/lib/routes/dealstreetasia/section.ts
new file mode 100644
index 00000000000000..d8fc0d7dbca592
--- /dev/null
+++ b/lib/routes/dealstreetasia/section.ts
@@ -0,0 +1,57 @@
+import { Route } from '@/types';
+// import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch'; // Unified request library used
+import { load } from 'cheerio'; // An HTML parser with an API similar to jQuery
+// import puppeteer from '@/utils/puppeteer';
+// import { parseDate } from '@/utils/parse-date';
+
+export const route: Route = {
+ path: '/section/:section',
+ categories: ['traditional-media'],
+ example: '/dealstreetasia/section/private-equity',
+ parameters: { section: 'target section' },
+ radar: [
+ {
+ source: ['dealstreetasia.com/'],
+ },
+ ],
+ name: 'Section',
+ maintainers: ['jack2game'],
+ handler,
+ url: 'dealstreetasia.com/',
+};
+
+async function handler(ctx) {
+ const section = ctx.req.param('section');
+ const items = await fetchPage(section);
+
+ return items;
+}
+
+async function fetchPage(section: string) {
+ const baseUrl = 'https://dealstreetasia.com'; // Define base URL
+
+ const response = await ofetch(`${baseUrl}/section/${section}/`);
+ const $ = load(response);
+
+ const jsonData = JSON.parse($('#__NEXT_DATA__').text());
+ const headingText = jsonData.props.pageProps.sectionData.name;
+
+ const items = jsonData.props.pageProps.sectionData.stories.nodes;
+
+ const feedItems = items.map((item) => ({
+ title: item.title || 'No Title',
+ link: item.uri ? `https://www.dealstreetasia.com${item.uri}` : '',
+ description: item.excerpt || '', // Default to empty string if undefined
+ pubDate: item.post_date ? new Date(item.post_date).toUTCString() : '',
+ category: item.sections.nodes.map((section) => section.name),
+ image: item.featuredImage?.node?.mediaItemUrl.replace(/\?.*$/, ''), // Use .replace to sanitize the image URL
+ }));
+
+ return {
+ title: 'Deal Street Asia - ' + headingText,
+ language: 'en',
+ item: feedItems,
+ link: 'https://dealstreetasia.com/section/' + section + '/',
+ };
+}
diff --git a/lib/routes/decrypt/index.ts b/lib/routes/decrypt/index.ts
new file mode 100644
index 00000000000000..31748e776834a4
--- /dev/null
+++ b/lib/routes/decrypt/index.ts
@@ -0,0 +1,115 @@
+import { Route, Data } from '@/types';
+import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch';
+import { parseDate } from '@/utils/parse-date';
+import { load } from 'cheerio';
+import logger from '@/utils/logger';
+import parser from '@/utils/rss-parser';
+
+export const route: Route = {
+ path: '/',
+ categories: ['finance'],
+ example: '/decrypt',
+ parameters: {},
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ name: 'News',
+ maintainers: ['pseudoyu'],
+ handler,
+ radar: [
+ {
+ source: ['decrypt.co/'],
+ target: '/',
+ },
+ ],
+ description: 'Get latest news from Decrypt.',
+};
+
+async function handler(ctx): Promise {
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20;
+ const rssUrl = 'https://decrypt.co/feed';
+
+ const feed = await parser.parseURL(rssUrl);
+
+ const items = await Promise.all(
+ feed.items
+ .filter((item) => item && item.link && !item.link.includes('/videos'))
+ .slice(0, limit)
+ .map((item) =>
+ cache.tryGet(`decrypt:article:${item.link}`, async () => {
+ if (!item.link) {
+ return {};
+ }
+
+ try {
+ const result = await extractFullText(item.link);
+ return {
+ title: item.title || 'Untitled',
+ link: item.link.split('?')[0], // Clean URL by removing query parameters
+ pubDate: item.pubDate ? parseDate(item.pubDate) : undefined,
+ description: result?.fullText ?? (item.content || ''),
+ author: item.creator || 'Decrypt',
+ category: result?.tags ? [...new Set([...(item.categories ?? []), ...result.tags])] : item.categories || [],
+ guid: item.guid || item.link,
+ image: result?.featuredImage ?? item.enclosure?.url,
+ };
+ } catch (error: any) {
+ logger.warn(`Couldn't fetch full content for ${item.link}: ${error.message}`);
+
+ // Fallback to RSS content
+ return {
+ title: item.title || 'Untitled',
+ link: item.link.split('?')[0],
+ pubDate: item.pubDate ? parseDate(item.pubDate) : undefined,
+ description: item.content || '',
+ author: item.creator || 'Decrypt',
+ category: item.categories || [],
+ guid: item.guid || item.link,
+ image: item.enclosure?.url,
+ };
+ }
+ })
+ )
+ );
+
+ return {
+ title: feed.title || 'Decrypt',
+ link: feed.link || 'https://decrypt.co',
+ description: feed.description || 'Latest news from Decrypt',
+ item: items,
+ language: feed.language || 'en',
+ image: feed.image?.url,
+ } as Data;
+}
+
+async function extractFullText(url: string): Promise<{ fullText: string; featuredImage: string; tags: string[] } | null> {
+ try {
+ const response = await ofetch(url);
+
+ const $ = load(response);
+
+ const nextData = JSON.parse($('script#__NEXT_DATA__').text());
+ const post = nextData.props.pageProps.post;
+
+ if (post.content.length) {
+ const fullText = `` + post.content;
+
+ return {
+ fullText,
+ featuredImage: post.featuredImage.src,
+ tags: post.tags.data.map((tag) => tag.name),
+ };
+ }
+
+ return null;
+ } catch (error) {
+ logger.error(`Error extracting full text from ${url}: ${error}`);
+ return null;
+ }
+}
diff --git a/lib/routes/decrypt/namespace.ts b/lib/routes/decrypt/namespace.ts
new file mode 100644
index 00000000000000..41ec11014cbb2e
--- /dev/null
+++ b/lib/routes/decrypt/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Decrypt',
+ url: 'decrypt.co',
+ lang: 'en',
+};
diff --git a/lib/routes/dedao/articles.ts b/lib/routes/dedao/articles.ts
new file mode 100644
index 00000000000000..58e4a9b0f92575
--- /dev/null
+++ b/lib/routes/dedao/articles.ts
@@ -0,0 +1,149 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+
+export const route: Route = {
+ path: '/articles/:id?',
+ categories: ['new-media'],
+ example: '/articles/9', // 示例路径更新
+ parameters: { id: '文章类型 ID,8 为得到头条,9 为得到精选,默认为 8' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['igetget.com'],
+ target: '/articles/:id',
+ },
+ ],
+ name: '得到文章',
+ maintainers: ['Jacky-Chen-Pro'],
+ handler,
+ url: 'www.igetget.com',
+};
+
+function handleParagraph(data) {
+ let html = '
` : '';
+}
+
+function handleHr() {
+ return '
';
+}
+
+function extractArticleContent(data) {
+ if (!data || typeof data !== 'object') {
+ return '';
+ }
+
+ switch (data.type) {
+ case 'paragraph':
+ return handleParagraph(data);
+ case 'text':
+ return handleText(data);
+ case 'image':
+ return handleImage(data);
+ case 'hr':
+ return handleHr();
+ default:
+ return '';
+ }
+}
+
+async function handler(ctx) {
+ const { id = '8' } = ctx.req.param();
+ const rootUrl = 'https://www.igetget.com';
+ const headers = {
+ Accept: 'application/json, text/plain, */*',
+ 'Content-Type': 'application/json;charset=UTF-8',
+ Referer: `https://m.igetget.com/share/course/free/detail?id=nb9L2q1e3OxKBPNsdoJrgN8P0Rwo6B`,
+ Origin: 'https://m.igetget.com',
+ };
+ const max_id = 0;
+
+ const response = await got.post('https://m.igetget.com/share/api/course/free/pageTurning', {
+ json: {
+ chapter_id: 0,
+ count: 5,
+ max_id,
+ max_order_num: 0,
+ pid: Number(id),
+ ptype: 24,
+ reverse: true,
+ since_id: 0,
+ since_order_num: 0,
+ },
+ headers,
+ });
+
+ const data = JSON.parse(response.body);
+ if (!data || !data.article_list) {
+ throw new Error('文章列表不存在或为空');
+ }
+
+ const articles = data.article_list;
+
+ const items = await Promise.all(
+ articles.map((article) => {
+ const postUrl = `https://m.igetget.com/share/course/article/article_id/${article.id}`;
+ const postTitle = article.title;
+ const postTime = new Date(article.publish_time * 1000).toUTCString();
+
+ return cache.tryGet(postUrl, async () => {
+ const detailResponse = await got.get(postUrl, { headers });
+ const $ = load(detailResponse.body);
+
+ const scriptTag = $('script')
+ .filter((_, el) => $(el).text()?.includes('window.__INITIAL_STATE__'))
+ .text();
+
+ if (scriptTag) {
+ const jsonStr = scriptTag.match(/window\.__INITIAL_STATE__\s*=\s*(\{.*\});/)?.[1];
+ if (jsonStr) {
+ const articleData = JSON.parse(jsonStr);
+
+ const description = JSON.parse(articleData.articleContent.content)
+ .map((data) => extractArticleContent(data))
+ .join('');
+
+ return {
+ title: postTitle,
+ link: postUrl,
+ description,
+ pubDate: postTime,
+ };
+ }
+ }
+ return null;
+ });
+ })
+ );
+
+ return {
+ title: `得到文章 - ${id === '8' ? '头条' : '精选'}`,
+ link: rootUrl,
+ item: items.filter(Boolean),
+ };
+}
diff --git a/lib/routes/dedao/index.ts b/lib/routes/dedao/index.ts
index d2b6150f6ec593..f2c573ee75a48e 100644
--- a/lib/routes/dedao/index.ts
+++ b/lib/routes/dedao/index.ts
@@ -6,8 +6,14 @@ import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/:category?',
- name: 'Unknown',
- maintainers: [],
+ name: '文章',
+ maintainers: ['nczitzk', 'pseudoyu'],
+ categories: ['new-media', 'popular'],
+ example: '/dedao',
+ parameters: { category: '分类,见下表,默认为`news`' },
+ description: `| 新闻 | 人物故事 | 视频 |
+| ---- | ---- | ---- |
+| news | figure | video |`,
handler,
};
@@ -23,10 +29,10 @@ async function handler(ctx) {
const data = JSON.parse(response.data.match(/window.__INITIAL_STATE__= (.*);<\/script>/)[1]);
- let items = (category === 'news' ? data.news : category === 'figure' ? data.figure : data.videoList).map((item) => ({
+ 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')[0].split('-').join('')}/${item.token}`,
}));
items = await Promise.all(
@@ -47,7 +53,7 @@ async function handler(ctx) {
);
return {
- title: `得到${category === 'video' ? '' : '大事件'} - ${category === 'news' ? '新闻' : category === 'figure' ? '人物故事' : '视频'}`,
+ title: `得到${category === 'video' ? '' : '大事件'} - ${category === 'news' ? '新闻' : (category === 'figure' ? '人物故事' : '视频')}`,
link: rootUrl,
item: items,
description: data.description,
diff --git a/lib/routes/dedao/knowledge.ts b/lib/routes/dedao/knowledge.ts
index 68ad5bae13a1a4..d6c6ca4accc646 100644
--- a/lib/routes/dedao/knowledge.ts
+++ b/lib/routes/dedao/knowledge.ts
@@ -9,7 +9,7 @@ import path from 'node:path';
export const route: Route = {
path: '/knowledge/:topic?/:type?',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/dedao/knowledge',
parameters: { topic: '话题 id,可在对应话题页 URL 中找到', type: '分享类型,`true` 指精选,`false` 指最新,默认为精选' },
features: {
diff --git a/lib/routes/dedao/list.ts b/lib/routes/dedao/list.ts
index 182608a9039979..cca1c51fba7c53 100644
--- a/lib/routes/dedao/list.ts
+++ b/lib/routes/dedao/list.ts
@@ -5,7 +5,7 @@ import { load } from 'cheerio';
export const route: Route = {
path: '/list/:category?',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/dedao/list/年度日更',
parameters: { category: '分类名,默认为年度日更' },
features: {
@@ -44,7 +44,7 @@ async function handler(ctx) {
url: listUrl,
});
- const currentUrl = `${rootUrl}${listResponse.data.match(new RegExp('' + category + '<\\/span><\\/a>'))[1].split('"')[0]}`;
+ const currentUrl = `${rootUrl}${listResponse.data.match(new RegExp('' + category + String.raw`<\/span><\/a>`))[1].split('"')[0]}`;
const currentResponse = await got({
method: 'get',
diff --git a/lib/routes/dedao/namespace.ts b/lib/routes/dedao/namespace.ts
index c77830eb8651cc..9a61a637f8007e 100644
--- a/lib/routes/dedao/namespace.ts
+++ b/lib/routes/dedao/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '得到',
url: 'dedao.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/dedao/user.ts b/lib/routes/dedao/user.ts
index 7e8a41bee4ecf2..c2b8fb4d9da2b9 100644
--- a/lib/routes/dedao/user.ts
+++ b/lib/routes/dedao/user.ts
@@ -15,7 +15,7 @@ const types = {
export const route: Route = {
path: '/user/:id/:type?',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/dedao/user/VkA5OqLX4RyGxmZRNBMlwBrDaJQ9og',
parameters: { id: '用户 id,可在对应用户主页 URL 中找到', type: '类型,见下表,默认为`0`,即动态' },
features: {
@@ -30,8 +30,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 动态 | 书评 | 视频 |
- | ---- | ---- | ---- |
- | 0 | 7 | 12 |`,
+| ---- | ---- | ---- |
+| 0 | 7 | 12 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/deepin/homepage.ts b/lib/routes/deepin/homepage.ts
index 9ba1aa6bfae995..19add2eee45741 100644
--- a/lib/routes/deepin/homepage.ts
+++ b/lib/routes/deepin/homepage.ts
@@ -9,10 +9,12 @@ export const route: Route = {
parameters: { user_id: 'user id' },
name: 'BBS Home Page',
maintainers: ['tensor-tech'],
- radar: {
- source: ['bbs.deepin.org/user/:user_id'],
- target: '/homepage/:user_id',
- },
+ radar: [
+ {
+ source: ['bbs.deepin.org/user/:user_id'],
+ target: '/homepage/:user_id',
+ },
+ ],
handler,
};
diff --git a/lib/routes/deepin/namespace.ts b/lib/routes/deepin/namespace.ts
index eae1d434b24f66..9222bac2855b79 100644
--- a/lib/routes/deepin/namespace.ts
+++ b/lib/routes/deepin/namespace.ts
@@ -6,4 +6,5 @@ export const namespace: Namespace = {
zh: {
name: '深度Linux',
},
+ lang: 'zh-CN',
};
diff --git a/lib/routes/deepin/thread.ts b/lib/routes/deepin/thread.ts
new file mode 100644
index 00000000000000..99c30b9ec01f3d
--- /dev/null
+++ b/lib/routes/deepin/thread.ts
@@ -0,0 +1,102 @@
+import { Route, DataItem } from '@/types';
+import ofetch from '@/utils/ofetch';
+import cache from '@/utils/cache';
+import { parseDate } from '@/utils/parse-date';
+
+export const route: Route = {
+ path: '/threads/:type?',
+ categories: ['bbs'],
+ example: '/deepin/threads/latest',
+ parameters: {
+ type: {
+ description: '主题类型',
+ options: [
+ {
+ value: 'hot',
+ label: '最热主题',
+ },
+ {
+ value: 'latest',
+ label: '最新主题',
+ },
+ ],
+ },
+ },
+ name: '首页主题列表',
+ maintainers: ['myml'],
+ radar: [
+ {
+ source: ['bbs.deepin.org'],
+ target: '/threads/latest',
+ },
+ ],
+ handler,
+};
+
+interface ThreadIndexResult {
+ ThreadIndex: {
+ id: number;
+ subject: string;
+ created_at: string;
+ user: { nickname: string };
+ forum: { name: string };
+ }[];
+}
+interface ThreadInfoResult {
+ data: {
+ id: number;
+ subject: string;
+ created_at: string;
+ user: { nickname: string };
+ post: { message: string };
+ };
+}
+
+const TypeMap = {
+ hot: { where: 'hot_value', title: '最热主题' },
+ latest: { where: 'id', title: '最新主题' },
+};
+
+async function handler(ctx) {
+ let type = TypeMap.latest;
+ if (ctx.req.param('type') === 'hot') {
+ type = TypeMap.hot;
+ }
+ const res = await ofetch
+
{{ intro }}
+{{ /if }}
+
+{{ if description }}
+ {{@ description }}
+{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/deeplearning/the-batch.ts b/lib/routes/deeplearning/the-batch.ts
new file mode 100644
index 00000000000000..5abe989dc12da0
--- /dev/null
+++ b/lib/routes/deeplearning/the-batch.ts
@@ -0,0 +1,296 @@
+import { Route } from '@/types';
+import { getCurrentPath } from '@/utils/helpers';
+const __dirname = getCurrentPath(import.meta.url);
+
+import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+import { art } from '@/utils/render';
+import path from 'node:path';
+
+export const handler = async (ctx) => {
+ const { tag } = ctx.req.param();
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 1;
+
+ const rootUrl = 'https://www.deeplearning.ai';
+ const currentUrl = new URL(`the-batch${tag ? `/tag/${tag.replace(/^tag\//, '').replace(/\/$/, '')}` : ''}/`, rootUrl).href;
+
+ const response = await ofetch(currentUrl);
+
+ const $ = load(response);
+
+ const language = $('html').prop('lang');
+
+ const data = JSON.parse($('script#__NEXT_DATA__').text());
+
+ const nextBuildId = data.buildId;
+ const posts = data.props?.pageProps?.posts ?? [];
+
+ let items = posts.slice(0, limit).map((item) => {
+ const title = item.title;
+ const description = art(path.join(__dirname, 'templates/description.art'), {
+ images: item.feature_image
+ ? [
+ {
+ src: item.feature_image,
+ alt: item.feature_image_alt,
+ },
+ ]
+ : undefined,
+ intro: item.excerpt ?? item.custom_excerpt,
+ });
+ const image = item.feature_image;
+ const guid = `the-batch-${item.slug}`;
+
+ return {
+ title,
+ description,
+ pubDate: parseDate(item.published_at),
+ link: new URL(`_next/data/${nextBuildId}/the-batch/${item.slug}.json`, rootUrl).href,
+ category: item.tags.map((t) => t.name),
+ guid,
+ id: guid,
+ content: {
+ html: description,
+ text: item.excerpt ?? item.custom_excerpt,
+ },
+ image,
+ banner: image,
+ language,
+ };
+ });
+
+ items = await Promise.all(
+ items.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const detailResponse = await ofetch(item.link);
+
+ const post = detailResponse.pageProps?.cmsData?.post ?? undefined;
+
+ if (!post) {
+ return item;
+ }
+
+ const $$ = load(post.html);
+
+ $$('a').each((_, ele) => {
+ if (ele.attribs.href?.includes('utm_campaign')) {
+ const url = new URL(ele.attribs.href);
+ url.searchParams.delete('utm_campaign');
+ url.searchParams.delete('utm_source');
+ url.searchParams.delete('utm_medium');
+ url.searchParams.delete('_hsenc');
+ ele.attribs.href = url.href;
+ }
+ });
+
+ const title = post.title;
+ const description = art(path.join(__dirname, 'templates/description.art'), {
+ images: post.feature_image
+ ? [
+ {
+ src: post.feature_image,
+ alt: post.feature_image_alt,
+ },
+ ]
+ : undefined,
+ intro: post.excerpt ?? post.custom_excerpt,
+ description: $$.html(),
+ });
+ const guid = `the-batch-${post.slug}`;
+ const image = post.feature_image;
+
+ item.title = title;
+ item.description = description;
+ item.pubDate = parseDate(post.published_at);
+ item.link = new URL(`the-batch/${post.slug}`, rootUrl).href;
+ item.category = post.tags.map((t) => t.name);
+ item.author = post.authors.map((a) => a.name).join('/');
+ item.guid = guid;
+ item.id = guid;
+ item.content = {
+ html: description,
+ text: post.excerpt ?? post.custom_excerpt,
+ };
+ item.image = image;
+ item.banner = image;
+ item.updated = parseDate(post.updated_at);
+ item.language = language;
+
+ return item;
+ })
+ )
+ );
+
+ const image = new URL($('meta[property="og:image"]').prop('content'), rootUrl).href;
+
+ return {
+ title: $('title').text(),
+ description: $('meta[property="og:description"]').prop('content'),
+ link: currentUrl,
+ item: items,
+ allowEmpty: true,
+ image,
+ author: $('meta[property="og:site_name"]').prop('content'),
+ language,
+ };
+};
+
+export const route: Route = {
+ path: '/the-batch/:tag{.+}?',
+ name: 'The Batch',
+ url: 'www.deeplearning.ai',
+ maintainers: ['nczitzk', 'juvenn', 'TonyRL'],
+ handler,
+ example: '/deeplearning/the-batch',
+ parameters: { tag: 'Tag, Weekly Issues by default' },
+ description: `::: tip
+ If you subscribe to [Data Points](https://www.deeplearning.ai/the-batch/tag/data-points/),where the URL is \`https://www.deeplearning.ai/the-batch/tag/data-points/\`, extract the part \`https://www.deeplearning.ai/the-batch/tag\` to the end, which is \`data-points\`, and use it as the parameter to fill in. Therefore, the route will be [\`/deeplearning/the-batch/data-points\`](https://rsshub.app/deeplearning/the-batch/data-points).
+
+:::
+
+| Tag | ID |
+| ---------------------------------------------------------------------- | -------------------------------------------------------------------- |
+| [Weekly Issues](https://www.deeplearning.ai/the-batch/) | [*null*](https://rsshub.app/deeplearning/the-batch) |
+| [Andrew's Letters](https://www.deeplearning.ai/the-batch/tag/letters/) | [letters](https://rsshub.app/deeplearning/the-batch/letters) |
+| [Data Points](https://www.deeplearning.ai/the-batch/tag/data-points/) | [data-points](https://rsshub.app/deeplearning/the-batch/data-points) |
+| [ML Research](https://www.deeplearning.ai/the-batch/tag/research/) | [research](https://rsshub.app/deeplearning/the-batch/research) |
+| [Business](https://www.deeplearning.ai/the-batch/tag/business/) | [business](https://rsshub.app/deeplearning/the-batch/business) |
+| [Science](https://www.deeplearning.ai/the-batch/tag/science/) | [science](https://rsshub.app/deeplearning/the-batch/science) |
+| [AI & Society](https://www.deeplearning.ai/the-batch/tag/ai-society/) | [ai-society](https://rsshub.app/deeplearning/the-batch/ai-society) |
+| [Culture](https://www.deeplearning.ai/the-batch/tag/culture/) | [culture](https://rsshub.app/deeplearning/the-batch/culture) |
+| [Hardware](https://www.deeplearning.ai/the-batch/tag/hardware/) | [hardware](https://rsshub.app/deeplearning/the-batch/hardware) |
+| [AI Careers](https://www.deeplearning.ai/the-batch/tag/ai-careers/) | [ai-careers](https://rsshub.app/deeplearning/the-batch/ai-careers) |
+
+#### [Letters from Andrew Ng](https://www.deeplearning.ai/the-batch/tag/letters/)
+
+| Tag | ID |
+| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
+| [All](https://www.deeplearning.ai/the-batch/tag/letters/) | [letters](https://rsshub.app/deeplearning/the-batch/letters) |
+| [Personal Insights](https://www.deeplearning.ai/the-batch/tag/personal-insights/) | [personal-insights](https://rsshub.app/deeplearning/the-batch/personal-insights) |
+| [Technical Insights](https://www.deeplearning.ai/the-batch/tag/technical-insights/) | [technical-insights](https://rsshub.app/deeplearning/the-batch/technical-insights) |
+| [Business Insights](https://www.deeplearning.ai/the-batch/tag/business-insights/) | [business-insights](https://rsshub.app/deeplearning/the-batch/business-insights) |
+| [Tech & Society](https://www.deeplearning.ai/the-batch/tag/tech-society/) | [tech-society](https://rsshub.app/deeplearning/the-batch/tech-society) |
+| [DeepLearning.AI News](https://www.deeplearning.ai/the-batch/tag/deeplearning-ai-news/) | [deeplearning-ai-news](https://rsshub.app/deeplearning/the-batch/deeplearning-ai-news) |
+| [AI Careers](https://www.deeplearning.ai/the-batch/tag/ai-careers/) | [ai-careers](https://rsshub.app/deeplearning/the-batch/ai-careers) |
+| [Just For Fun](https://www.deeplearning.ai/the-batch/tag/just-for-fun/) | [just-for-fun](https://rsshub.app/deeplearning/the-batch/just-for-fun) |
+| [Learning & Education](https://www.deeplearning.ai/the-batch/tag/learning-education/) | [learning-education](https://rsshub.app/deeplearning/the-batch/learning-education) |
+ `,
+ categories: ['programming'],
+
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['www.deeplearning.ai/the-batch', 'www.deeplearning.ai/the-batch/tag/:tag/'],
+ target: (params) => {
+ const tag = params.tag;
+
+ return `/the-batch${tag ? `/${tag}` : ''}`;
+ },
+ },
+ {
+ title: 'Weekly Issues',
+ source: ['www.deeplearning.ai/the-batch/'],
+ target: '/the-batch',
+ },
+ {
+ title: "Andrew's Letters",
+ source: ['www.deeplearning.ai/the-batch/tag/letters/'],
+ target: '/the-batch/letters',
+ },
+ {
+ title: 'Data Points',
+ source: ['www.deeplearning.ai/the-batch/tag/data-points/'],
+ target: '/the-batch/data-points',
+ },
+ {
+ title: 'ML Research',
+ source: ['www.deeplearning.ai/the-batch/tag/research/'],
+ target: '/the-batch/research',
+ },
+ {
+ title: 'Business',
+ source: ['www.deeplearning.ai/the-batch/tag/business/'],
+ target: '/the-batch/business',
+ },
+ {
+ title: 'Science',
+ source: ['www.deeplearning.ai/the-batch/tag/science/'],
+ target: '/the-batch/science',
+ },
+ {
+ title: 'AI & Society',
+ source: ['www.deeplearning.ai/the-batch/tag/ai-society/'],
+ target: '/the-batch/ai-society',
+ },
+ {
+ title: 'Culture',
+ source: ['www.deeplearning.ai/the-batch/tag/culture/'],
+ target: '/the-batch/culture',
+ },
+ {
+ title: 'Hardware',
+ source: ['www.deeplearning.ai/the-batch/tag/hardware/'],
+ target: '/the-batch/hardware',
+ },
+ {
+ title: 'AI Careers',
+ source: ['www.deeplearning.ai/the-batch/tag/ai-careers/'],
+ target: '/the-batch/ai-careers',
+ },
+ {
+ title: 'Letters from Andrew Ng - All',
+ source: ['www.deeplearning.ai/the-batch/tag/letters/'],
+ target: '/the-batch/letters',
+ },
+ {
+ title: 'Letters from Andrew Ng - Personal Insights',
+ source: ['www.deeplearning.ai/the-batch/tag/personal-insights/'],
+ target: '/the-batch/personal-insights',
+ },
+ {
+ title: 'Letters from Andrew Ng - Technical Insights',
+ source: ['www.deeplearning.ai/the-batch/tag/technical-insights/'],
+ target: '/the-batch/technical-insights',
+ },
+ {
+ title: 'Letters from Andrew Ng - Business Insights',
+ source: ['www.deeplearning.ai/the-batch/tag/business-insights/'],
+ target: '/the-batch/business-insights',
+ },
+ {
+ title: 'Letters from Andrew Ng - Tech & Society',
+ source: ['www.deeplearning.ai/the-batch/tag/tech-society/'],
+ target: '/the-batch/tech-society',
+ },
+ {
+ title: 'Letters from Andrew Ng - DeepLearning.AI News',
+ source: ['www.deeplearning.ai/the-batch/tag/deeplearning-ai-news/'],
+ target: '/the-batch/deeplearning-ai-news',
+ },
+ {
+ title: 'Letters from Andrew Ng - AI Careers',
+ source: ['www.deeplearning.ai/the-batch/tag/ai-careers/'],
+ target: '/the-batch/ai-careers',
+ },
+ {
+ title: 'Letters from Andrew Ng - Just For Fun',
+ source: ['www.deeplearning.ai/the-batch/tag/just-for-fun/'],
+ target: '/the-batch/just-for-fun',
+ },
+ {
+ title: 'Letters from Andrew Ng - Learning & Education',
+ source: ['www.deeplearning.ai/the-batch/tag/learning-education/'],
+ target: '/the-batch/learning-education',
+ },
+ ],
+};
diff --git a/lib/routes/deeplearning/thebatch.ts b/lib/routes/deeplearning/thebatch.ts
deleted file mode 100644
index 1264a03a051bcb..00000000000000
--- a/lib/routes/deeplearning/thebatch.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Route } from '@/types';
-import cache from '@/utils/cache';
-import got from '@/utils/got';
-
-export const route: Route = {
- path: '/thebatch',
- categories: ['programming'],
- example: '/deeplearning/thebatch',
- parameters: {},
- features: {
- requireConfig: false,
- requirePuppeteer: false,
- antiCrawler: false,
- supportBT: false,
- supportPodcast: false,
- supportScihub: false,
- },
- radar: [
- {
- source: ['www.deeplearning.ai/thebatch', 'www.deeplearning.ai/'],
- },
- ],
- name: 'TheBatch 周报',
- maintainers: ['nczitzk', 'juvenn'],
- handler,
- url: 'www.deeplearning.ai/thebatch',
-};
-
-async function handler() {
- const page = await got({
- method: 'get',
- url: `https://www.deeplearning.ai/the-batch/`,
- });
- const nextJs = page.data.match(/', '')
+ .replaceAll('*业务咨询* 和 *投诉建议* 可用的站点参数
+*业务咨询* 和 *投诉建议* 可用的站点参数
- | 上海市 | 北京市 | 天津市 | 河北省 |
- | -------- | ------- | ------- | ------ |
- | shanghai | beijing | tianjin | hebei |
+| 上海市 | 北京市 | 天津市 | 河北省 |
+| -------- | ------- | ------- | ------ |
+| shanghai | beijing | tianjin | hebei |
- | 山西省 | 内蒙古自治区 | 辽宁省 | 吉林省 |
- | ------ | ------------ | -------- | ------ |
- | shanxi | neimenggu | liaoning | jilin |
+| 山西省 | 内蒙古自治区 | 辽宁省 | 吉林省 |
+| ------ | ------------ | -------- | ------ |
+| shanxi | neimenggu | liaoning | jilin |
- | 黑龙江省 | 江苏省 | 浙江省 | 安徽省 |
- | ------------ | ------- | -------- | ------ |
- | heilongjiang | jiangsu | zhejiang | anhui |
+| 黑龙江省 | 江苏省 | 浙江省 | 安徽省 |
+| ------------ | ------- | -------- | ------ |
+| heilongjiang | jiangsu | zhejiang | anhui |
- | 福建省 | 江西省 | 山东省 | 河南省 |
- | ------ | ------- | -------- | ------ |
- | fujian | jiangxi | shandong | henan |
+| 福建省 | 江西省 | 山东省 | 河南省 |
+| ------ | ------- | -------- | ------ |
+| fujian | jiangxi | shandong | henan |
- | 湖北省 | 湖南省 | 广东省 | 广西壮族自治区 |
- | ------ | ------ | --------- | -------------- |
- | hubei | hunan | guangdong | guangxi |
+| 湖北省 | 湖南省 | 广东省 | 广西壮族自治区 |
+| ------ | ------ | --------- | -------------- |
+| hubei | hunan | guangdong | guangxi |
- | 海南省 | 重庆市 | 四川省 | 贵州省 |
- | ------ | --------- | ------- | ------- |
- | hainan | chongqing | sichuan | guizhou |
+| 海南省 | 重庆市 | 四川省 | 贵州省 |
+| ------ | --------- | ------- | ------- |
+| hainan | chongqing | sichuan | guizhou |
- | 云南省 | 西藏自治区 | 陕西省 | 甘肃省 |
- | ------ | ---------- | ------- | ------ |
- | yunnan | xizang | shaanxi | gansu |
+| 云南省 | 西藏自治区 | 陕西省 | 甘肃省 |
+| ------ | ---------- | ------- | ------ |
+| yunnan | xizang | shaanxi | gansu |
- | 青海省 | 宁夏回族自治区 | 新疆维吾尔自治区 | 大连市 |
- | ------- | -------------- | ---------------- | ------ |
- | qinghai | ningxia | xinjiang | dalian |
+| 青海省 | 宁夏回族自治区 | 新疆维吾尔自治区 | 大连市 |
+| ------- | -------------- | ---------------- | ------ |
+| qinghai | ningxia | xinjiang | dalian |
- | 宁波市 | 厦门市 | 青岛市 | 深圳市 |
- | ------ | ------ | ------- | -------- |
- | ningbo | xiamen | qingdao | shenzhen |
+| 宁波市 | 厦门市 | 青岛市 | 深圳市 |
+| ------ | ------ | ------- | -------- |
+| ningbo | xiamen | qingdao | shenzhen |
+
${noticeCate}
+ ${$('a').attr('title')}
+ `,
+ };
+ });
+ return {
+ title: '天津港保税区-公告',
+ link: url,
+ item,
+ };
+ },
+};
diff --git a/lib/routes/gov/tianjin/tjrcgzw.ts b/lib/routes/gov/tianjin/tjrcgzw.ts
new file mode 100644
index 00000000000000..55af4408ce5b22
--- /dev/null
+++ b/lib/routes/gov/tianjin/tjrcgzw.ts
@@ -0,0 +1,51 @@
+import { Route } from '@/types';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+export const route: Route = {
+ path: '/tianjin/tjrcgzw-notice/:cate/:subCate',
+ categories: ['government'],
+ example: '/gov/tianjin/tjrcgzw-notice/rczc/sjrczc/',
+ parameters: {
+ channelId: '公告分类id、详细信息点击源网站https://hrss.tj.gov.cn/ztzl/ztzl1/tjrcgzw/请求中寻找',
+ },
+ radar: [
+ {
+ source: ['hrss.tj.gov.cn/ztzl/ztzl1/tjrcgzw/'],
+ target: '/tianjin/tjrcgzw-notice/:cate/:subCate',
+ },
+ ],
+ name: '天津人才工作网-公告',
+ url: 'hrss.tj.gov.cn/ztzl/ztzl1/tjrcgzw/',
+ maintainers: ['HaoyuLee'],
+ async handler(ctx) {
+ const { cate, subCate } = ctx.req.param();
+ const url = `https://hrss.tj.gov.cn/ztzl/ztzl1/tjrcgzw/${cate}/${subCate}/`;
+ const { data: response } = await got(url);
+ const noticeCate = load(response)('.routeBlockAuto').text().trim();
+ const item = load(response)('ul.listUlBox01>li')
+ .toArray()
+ .map((el) => {
+ const $ = load(el);
+ const title = $('a').text().trim();
+ const href = $('a').attr('href') || '';
+ const date = $('span').text().trim();
+ const link = href!.includes('http') ? href : new URL(href, url).href;
+ return {
+ title: `天津人才工作网:${title}`,
+ link,
+ pubDate: parseDate(date),
+ author: '天津人才工作网',
+ description: `
+ ${noticeCate}
+ ${title}
+ `,
+ };
+ });
+ return {
+ title: '天津人才工作网-公告',
+ link: url,
+ item,
+ };
+ },
+};
diff --git a/lib/routes/gov/xuzhou/hrss.ts b/lib/routes/gov/xuzhou/hrss.ts
index 030893e19996b7..8855ad0729251d 100644
--- a/lib/routes/gov/xuzhou/hrss.ts
+++ b/lib/routes/gov/xuzhou/hrss.ts
@@ -22,8 +22,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 通知公告 | 要闻动态 | 县区动态 | 事业招聘 | 企业招聘 | 政声传递 |
- | -------- | -------- | -------- | -------- | -------- | -------- |
- | | 001001 | 001002 | 001004 | 001005 | 001006 |`,
+| -------- | -------- | -------- | -------- | -------- | -------- |
+| | 001001 | 001002 | 001004 | 001005 | 001006 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/gov/zhejiang/gwy.ts b/lib/routes/gov/zhejiang/gwy.ts
index e7480327a03ed4..e0276fb6daa2de 100644
--- a/lib/routes/gov/zhejiang/gwy.ts
+++ b/lib/routes/gov/zhejiang/gwy.ts
@@ -28,28 +28,28 @@ export const route: Route = {
handler,
url: 'zjks.gov.cn/zjgwy/website/init.htm',
description: `| 分类 | id |
- | ------------ | -- |
- | 重要通知 | 1 |
- | 招考公告 | 2 |
- | 招考政策 | 3 |
- | 面试体检考察 | 4 |
- | 录用公示专栏 | 5 |
-
- | 地市 | id |
- | ------------ | ----- |
- | 浙江省 | 133 |
- | 浙江省杭州市 | 13301 |
- | 浙江省宁波市 | 13302 |
- | 浙江省温州市 | 13303 |
- | 浙江省嘉兴市 | 13304 |
- | 浙江省湖州市 | 13305 |
- | 浙江省绍兴市 | 13306 |
- | 浙江省金华市 | 13307 |
- | 浙江省衢州市 | 13308 |
- | 浙江省舟山市 | 13309 |
- | 浙江省台州市 | 13310 |
- | 浙江省丽水市 | 13311 |
- | 省级单位 | 13317 |`,
+| ------------ | -- |
+| 重要通知 | 1 |
+| 招考公告 | 2 |
+| 招考政策 | 3 |
+| 面试体检考察 | 4 |
+| 录用公示专栏 | 5 |
+
+| 地市 | id |
+| ------------ | ----- |
+| 浙江省 | 133 |
+| 浙江省杭州市 | 13301 |
+| 浙江省宁波市 | 13302 |
+| 浙江省温州市 | 13303 |
+| 浙江省嘉兴市 | 13304 |
+| 浙江省湖州市 | 13305 |
+| 浙江省绍兴市 | 13306 |
+| 浙江省金华市 | 13307 |
+| 浙江省衢州市 | 13308 |
+| 浙江省舟山市 | 13309 |
+| 浙江省台州市 | 13310 |
+| 浙江省丽水市 | 13311 |
+| 省级单位 | 13317 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/gov/zhengce/govall.ts b/lib/routes/gov/zhengce/govall.ts
index 77e9a6ed4257f8..fe3fb7da1d5b2c 100644
--- a/lib/routes/gov/zhengce/govall.ts
+++ b/lib/routes/gov/zhengce/govall.ts
@@ -29,15 +29,15 @@ export const route: Route = {
handler,
url: 'www.gov.cn/',
description: `| 选项 | 意义 | 备注 |
- | :-----------------------------: | :----------------------------------------------: | :----------------------------: |
- | orpro | 包含以下任意一个关键词。 | 用空格分隔。 |
- | allpro | 包含以下全部关键词 | |
- | notpro | 不包含以下关键词 | |
- | inpro | 完整不拆分的关键词 | |
- | searchfield | title: 搜索词在标题中;content: 搜索词在正文中。 | 默认为空,即网页的任意位置。 |
- | pubmintimeYear, pubmintimeMonth | 从某年某月 | 单独使用月份参数无法只筛选月份 |
- | pubmaxtimeYear, pubmaxtimeMonth | 到某年某月 | 单独使用月份参数无法只筛选月份 |
- | colid | 栏目 | 比较复杂,不建议使用 |`,
+| :-----------------------------: | :----------------------------------------------: | :----------------------------: |
+| orpro | 包含以下任意一个关键词。 | 用空格分隔。 |
+| allpro | 包含以下全部关键词 | |
+| notpro | 不包含以下关键词 | |
+| inpro | 完整不拆分的关键词 | |
+| searchfield | title: 搜索词在标题中;content: 搜索词在正文中。 | 默认为空,即网页的任意位置。 |
+| pubmintimeYear, pubmintimeMonth | 从某年某月 | 单独使用月份参数无法只筛选月份 |
+| pubmaxtimeYear, pubmaxtimeMonth | 到某年某月 | 单独使用月份参数无法只筛选月份 |
+| colid | 栏目 | 比较复杂,不建议使用 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/gov/zhengce/index.ts b/lib/routes/gov/zhengce/index.ts
index a1e45ef7ad2704..1c648fc547fd85 100644
--- a/lib/routes/gov/zhengce/index.ts
+++ b/lib/routes/gov/zhengce/index.ts
@@ -70,19 +70,16 @@ async function handler(ctx) {
const agencyEl = content('table.bd1')
.find('td')
.toArray()
- .filter((a) => content(a).text().startsWith('发文机关'))
- .pop();
+ .findLast((a) => content(a).text().startsWith('发文机关'));
const sourceEl = content('span.font-zyygwj')
.toArray()
- .filter((a) => content(a).text().startsWith('来源'))
- .pop();
+ .findLast((a) => content(a).text().startsWith('来源'));
const subjectEl = content('table.bd1')
.find('td')
.toArray()
- .filter((a) => content(a).text().startsWith('主题分类'))
- .pop();
+ .findLast((a) => content(a).text().startsWith('主题分类'));
const agency = agencyEl ? processElementText(agencyEl) : undefined;
const source = sourceEl ? processElementText(sourceEl) : undefined;
diff --git a/lib/routes/gov/zj/ningbogzw-notice.ts b/lib/routes/gov/zj/ningbogzw-notice.ts
new file mode 100644
index 00000000000000..f3454f46c05731
--- /dev/null
+++ b/lib/routes/gov/zj/ningbogzw-notice.ts
@@ -0,0 +1,50 @@
+import { Route } from '@/types';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+
+export const route: Route = {
+ path: '/zj/ningbogzw-notice/:colId?',
+ categories: ['government'],
+ example: '/gov/zj/ningbogzw-notice/1229116730',
+ parameters: {
+ colId: '公告分类id、详细信息点击源网站http://gzw.ningbo.gov.cn/请求中寻找',
+ },
+ radar: [
+ {
+ source: ['gzw.ningbo.gov.cn/col/col1229116730/index.html'],
+ target: '/zj/ningbogzw-notice/:colId?',
+ },
+ ],
+ name: '宁波市国资委-公告',
+ url: 'gzw.ningbo.gov.cn',
+ maintainers: ['HaoyuLee'],
+ description: `
+| 公告类别 | colId |
+| ------------ | -- |
+| 首页-市属国企招聘信息-招聘公告 | 1229116730 |
+ `,
+ async handler(ctx) {
+ const { colId = '1229116730' } = ctx.req.param();
+ const url = `http://gzw.ningbo.gov.cn/col/col${colId}/index.html`;
+ const { data: response } = await got(url);
+ const noticeCate = load(response)('.List-topic .text-tag').text().trim();
+ const reg = /更多分类
+
+| 分类 | ID |
+| -------- | ------------------ |
+| 政务信息 | newsListHome/1430 |
+| 要闻动态 | newsListHome/3 |
+| 产业经济 | newsListHome/1469 |
+| 产业信息 | newsListHome/1471 |
+| 爱粮节粮 | newsListHome/1470 |
+| 政策法规 | newsListChannel/18 |
+| 生产气象 | newsListChannel/19 |
+| 统计资料 | newsListChannel/20 |
+| 综合信息 | newsListChannel/21 |
+
+
`;
}
diff --git a/lib/routes/guancha/namespace.ts b/lib/routes/guancha/namespace.ts
index 5261bf8da1f3e1..dc7967a68620b5 100644
--- a/lib/routes/guancha/namespace.ts
+++ b/lib/routes/guancha/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '观察者网',
url: 'guancha.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/guangdiu/index.ts b/lib/routes/guangdiu/index.ts
index 55186bdd951aa1..29c4fa281edf77 100644
--- a/lib/routes/guangdiu/index.ts
+++ b/lib/routes/guangdiu/index.ts
@@ -22,9 +22,9 @@ export const route: Route = {
name: '国内折扣 / 海外折扣',
maintainers: ['Fatpandac'],
handler,
- description: `:::tip
+ description: `::: tip
海外折扣: [\`/guangdiu/k=daily&c=us\`](https://rsshub.app/guangdiu/k=daily\&c=us)
- :::`,
+:::`,
};
async function handler(ctx) {
diff --git a/lib/routes/guangdiu/namespace.ts b/lib/routes/guangdiu/namespace.ts
index 58969c86d7fa91..791f0df07c7376 100644
--- a/lib/routes/guangdiu/namespace.ts
+++ b/lib/routes/guangdiu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '逛丢',
url: 'guangdiu.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/guangdiu/search.ts b/lib/routes/guangdiu/search.ts
index dd2038f3455a56..413ca7e205423f 100644
--- a/lib/routes/guangdiu/search.ts
+++ b/lib/routes/guangdiu/search.ts
@@ -9,7 +9,7 @@ const host = 'https://guangdiu.com';
export const route: Route = {
path: '/search/:query?',
categories: ['shopping'],
- example: '/guangdiu/search/k=百度网盘',
+ example: '/guangdiu/search/q=百度网盘',
parameters: { query: '链接参数,对应网址问号后的内容' },
features: {
requireConfig: false,
diff --git a/lib/routes/guangzhoumetro/namespace.ts b/lib/routes/guangzhoumetro/namespace.ts
index 5531e4b17782db..afdc30c4bfea1a 100644
--- a/lib/routes/guangzhoumetro/namespace.ts
+++ b/lib/routes/guangzhoumetro/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '广州地铁',
url: 'www.gzmtr.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/guanhai/namespace.ts b/lib/routes/guanhai/namespace.ts
index db355e9b828cbc..7947de6c9326d6 100644
--- a/lib/routes/guanhai/namespace.ts
+++ b/lib/routes/guanhai/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '观海新闻',
url: 'guanhai.com.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/guduodata/daily.ts b/lib/routes/guduodata/daily.ts
index 86d4fbedee3e65..f8dc6986c269b3 100644
--- a/lib/routes/guduodata/daily.ts
+++ b/lib/routes/guduodata/daily.ts
@@ -8,7 +8,7 @@ import dayjs from 'dayjs';
import { art } from '@/utils/render';
import path from 'node:path';
-const host = 'http://data.guduodata.com';
+const host = 'http://d.guduodata.com';
const types = {
collect: {
@@ -46,13 +46,13 @@ export const route: Route = {
},
radar: [
{
- source: ['data.guduodata.com/'],
+ source: ['guduodata.com/'],
},
],
name: '日榜',
maintainers: ['Gem1ni'],
handler,
- url: 'data.guduodata.com/',
+ url: 'guduodata.com/',
};
async function handler() {
@@ -65,7 +65,7 @@ async function handler() {
type: key,
name: `[${yestoday}] ${types[key].name} - ${types[key].categories[category]}`,
category: category.toUpperCase(),
- url: `${host}/show/datalist?type=DAILY&category=${category.toUpperCase()}&date=${yestoday}`,
+ url: `${host}/m/v3/billboard/list?type=DAILY&category=${category.toUpperCase()}&date=${yestoday}`,
}))
);
return {
@@ -76,7 +76,7 @@ async function handler() {
items.map((item) =>
cache.tryGet(item.url, async () => {
const response = await got.get(`${item.url}&t=${now}`, {
- headers: { Referer: `http://data.guduodata.com/` },
+ headers: { Referer: `http://guduodata.com/` },
});
const data = response.data.data;
return {
diff --git a/lib/routes/guduodata/namespace.ts b/lib/routes/guduodata/namespace.ts
index e9e4d4dc0248ad..82d46310eec2c9 100644
--- a/lib/routes/guduodata/namespace.ts
+++ b/lib/routes/guduodata/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '骨朵数据',
url: 'data.guduodata.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/gumroad/namespace.ts b/lib/routes/gumroad/namespace.ts
index 2caaa70849482d..8266353e3d93f1 100644
--- a/lib/routes/gumroad/namespace.ts
+++ b/lib/routes/gumroad/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Gumroad',
url: 'gumroad.com',
+ lang: 'en',
};
diff --git a/lib/routes/guokr/channel.ts b/lib/routes/guokr/channel.ts
index 1d7c962bbf2749..1b19a6eb30e7ee 100644
--- a/lib/routes/guokr/channel.ts
+++ b/lib/routes/guokr/channel.ts
@@ -12,7 +12,7 @@ const channelMap = {
export const route: Route = {
path: '/column/:channel',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/guokr/column/calendar',
parameters: { channel: '专栏类别' },
radar: [
@@ -25,8 +25,8 @@ export const route: Route = {
handler,
url: 'guokr.com/',
description: `| 物种日历 | 吃货研究所 | 美丽也是技术活 |
- | -------- | ---------- | -------------- |
- | calendar | institute | beauty |`,
+| -------- | ---------- | -------------- |
+| calendar | institute | beauty |`,
};
async function handler(ctx) {
diff --git a/lib/routes/guokr/namespace.ts b/lib/routes/guokr/namespace.ts
index afd61d4463b77a..33bfe7c89a3f99 100644
--- a/lib/routes/guokr/namespace.ts
+++ b/lib/routes/guokr/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '果壳网',
url: 'guokr.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/guokr/scientific.ts b/lib/routes/guokr/scientific.ts
index 691f1cc4fbb051..bf3398e5ce29db 100644
--- a/lib/routes/guokr/scientific.ts
+++ b/lib/routes/guokr/scientific.ts
@@ -4,7 +4,7 @@ import { parseList, parseItem } from './utils';
export const route: Route = {
path: '/scientific',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/guokr/scientific',
radar: [
{
diff --git a/lib/routes/guokr/utils.ts b/lib/routes/guokr/utils.ts
index 8b0ed9d5f32d4a..7fb1a041a72801 100644
--- a/lib/routes/guokr/utils.ts
+++ b/lib/routes/guokr/utils.ts
@@ -23,7 +23,7 @@ export const parseItem = (item) =>
$('#meta_content').remove();
$('div').each((_, elem) => {
const $elem = $(elem);
- $elem.attr('style', $elem.attr('style')?.replaceAll(/display:none;|visibility: hidden;/g, ''));
+ $elem.attr('style', $elem.attr('style')?.replaceAll(/(?:display:\s*none|visibility:\s*hidden|opacity:\s*0);?/g, ''));
});
$('img').each((_, elem) => {
const $elem = $(elem);
diff --git a/lib/routes/guozaoke/index.ts b/lib/routes/guozaoke/index.ts
new file mode 100644
index 00000000000000..d8fe2c35e0a848
--- /dev/null
+++ b/lib/routes/guozaoke/index.ts
@@ -0,0 +1,99 @@
+import { Route } from '@/types';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseRelativeDate } from '@/utils/parse-date';
+import { config } from '@/config';
+import cache from '@/utils/cache';
+import asyncPool from 'tiny-async-pool';
+
+export const route: Route = {
+ path: '/default',
+ categories: ['bbs'],
+ example: '/guozaoke/default',
+ parameters: {},
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ name: '过早客',
+ maintainers: ['xiaoshame'],
+ handler,
+ url: 'guozaoke.com/',
+};
+
+async function handler() {
+ const url = 'https://www.guozaoke.com/';
+ const res = await got({
+ method: 'get',
+ url,
+ headers: {
+ Cookie: config.guozaoke.cookies,
+ 'User-Agent': config.ua,
+ },
+ });
+ const $ = load(res.data);
+
+ const list = $('div.topic-item').toArray();
+ const maxItems = 20; // 最多取20个数据
+
+ const items = list
+ .slice(0, maxItems)
+ .map((item) => {
+ const $item = $(item);
+ const title = $item.find('h3.title a').text();
+ const url = $item.find('h3.title a').attr('href');
+ const author = $item.find('span.username a').text();
+ const lastTouched = $item.find('span.last-touched').text();
+ const pubDate = parseRelativeDate(lastTouched);
+ const link = url ? url.split('#')[0] : undefined;
+ return link ? { title, link, author, pubDate } : undefined;
+ })
+ .filter((item) => item !== undefined);
+
+ const out = [];
+ for await (const result of asyncPool(2, items, (item) =>
+ cache.tryGet(item.link, async () => {
+ const url = `https://www.guozaoke.com${item.link}`;
+ const res = await got({
+ method: 'get',
+ url,
+ headers: {
+ Cookie: config.guozaoke.cookies,
+ 'User-Agent': config.ua,
+ },
+ });
+
+ const $ = load(res.data);
+ let content = $('div.ui-content').html();
+ content = content ? content.trim() : '';
+ const comments = $('.reply-item').map((i, el) => {
+ const $el = $(el);
+ const comment = $el.find('span.content').text().trim();
+ const author = $el.find('span.username').text();
+ return {
+ comment,
+ author,
+ };
+ });
+ if (comments && comments.length > 0) {
+ for (const item of comments) {
+ content += '
' + item.author + ': ' + item.comment;
+ }
+ }
+ item.description = content;
+ return item;
+ })
+ )) {
+ out.push(result);
+ }
+
+ return {
+ title: '过早客',
+ link: url,
+ item: out,
+ };
+}
diff --git a/lib/routes/guozaoke/namespace.ts b/lib/routes/guozaoke/namespace.ts
new file mode 100644
index 00000000000000..bf2275811f5a60
--- /dev/null
+++ b/lib/routes/guozaoke/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'guozaoke',
+ url: 'guozaoke.com',
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/gxmzu/namespace.ts b/lib/routes/gxmzu/namespace.ts
index 378bbaa0ace006..63abd273436b0a 100644
--- a/lib/routes/gxmzu/namespace.ts
+++ b/lib/routes/gxmzu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '广西民族大学',
url: 'ai.gxmzu.edu.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/gzdaily/app.ts b/lib/routes/gzdaily/app.ts
index 528f78aefd69d2..2dfaaf02d40c8a 100644
--- a/lib/routes/gzdaily/app.ts
+++ b/lib/routes/gzdaily/app.ts
@@ -26,19 +26,19 @@ export const route: Route = {
name: '客户端',
maintainers: ['TimWu007'],
handler,
- description: `:::tip
+ description: `::: tip
在北京时间深夜可能无法获取内容。
- :::
+:::
常用栏目 ID:
- | 栏目名 | ID |
- | ------ | ---- |
- | 首页 | 74 |
- | 时局 | 374 |
- | 广州 | 371 |
- | 大湾区 | 397 |
- | 城区 | 2980 |`,
+| 栏目名 | ID |
+| ------ | ---- |
+| 首页 | 74 |
+| 时局 | 374 |
+| 广州 | 371 |
+| 大湾区 | 397 |
+| 城区 | 2980 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/gzdaily/namespace.ts b/lib/routes/gzdaily/namespace.ts
index 967219dc8f78c9..ad480832a63b5a 100644
--- a/lib/routes/gzdaily/namespace.ts
+++ b/lib/routes/gzdaily/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '广州日报',
url: 'gzdaily.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/gzhu/namespace.ts b/lib/routes/gzhu/namespace.ts
index e9a2989cfc64ca..7993b047e5c68c 100644
--- a/lib/routes/gzhu/namespace.ts
+++ b/lib/routes/gzhu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '广州大学',
url: 'yjsy.gzhu.edu.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hackernews/index.ts b/lib/routes/hackernews/index.ts
index 259a3ff22c1c52..d62784fa895ab5 100644
--- a/lib/routes/hackernews/index.ts
+++ b/lib/routes/hackernews/index.ts
@@ -1,4 +1,4 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
@@ -6,9 +6,20 @@ import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/:section?/:type?/:user?',
- categories: ['programming'],
+ categories: ['programming', 'popular'],
+ view: ViewType.Articles,
example: '/hackernews/threads/comments_list/dang',
- parameters: { section: '内容分区,见上表,默认为 `index`', type: '链接类型,见上表,默认为 `sources`', user: '设定用户,只在 `threads` 和 `submitted` 分区有效' },
+ parameters: {
+ section: {
+ description: 'Content section, default to `index`',
+ },
+ type: {
+ description: 'Link type, default to `sources`',
+ },
+ user: {
+ description: 'Set user, only valid in `threads` and `submitted` sections',
+ },
+ },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -22,10 +33,10 @@ export const route: Route = {
source: ['news.ycombinator.com/:section', 'news.ycombinator.com/'],
},
],
- name: '用户',
+ name: 'User',
maintainers: ['nczitzk', 'xie-dongping'],
handler,
- description: `订阅特定用户的内容`,
+ description: `Subscribe to the content of a specific user`,
};
async function handler(ctx) {
diff --git a/lib/routes/hackernews/namespace.ts b/lib/routes/hackernews/namespace.ts
index fc27645c113ae5..89d38a3bd90669 100644
--- a/lib/routes/hackernews/namespace.ts
+++ b/lib/routes/hackernews/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Hacker News',
url: 'ycombinator.com',
+ lang: 'en',
};
diff --git a/lib/routes/hackertalk/namespace.ts b/lib/routes/hackertalk/namespace.ts
index 1e9f836931c1a1..23c607cc98202f 100644
--- a/lib/routes/hackertalk/namespace.ts
+++ b/lib/routes/hackertalk/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'HACKER TALK 黑客说',
url: 'hackertalk.net',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hacking8/index.ts b/lib/routes/hacking8/index.ts
index 18b97b197c37a0..70ac38a7fc1983 100644
--- a/lib/routes/hacking8/index.ts
+++ b/lib/routes/hacking8/index.ts
@@ -26,8 +26,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| 推荐 | 最近更新 | 漏洞 / PoC 监控 | PDF |
- | ----- | -------- | --------------- | --- |
- | likes | index | vul-poc | pdf |`,
+| ----- | -------- | --------------- | --- |
+| likes | index | vul-poc | pdf |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hacking8/namespace.ts b/lib/routes/hacking8/namespace.ts
index fed1fe3b40436b..0052f7446f96ef 100644
--- a/lib/routes/hacking8/namespace.ts
+++ b/lib/routes/hacking8/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Hacking8',
url: 'hacking8.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hackmd/namespace.ts b/lib/routes/hackmd/namespace.ts
index f463a6b7d77e5c..8b4a852d4eb612 100644
--- a/lib/routes/hackmd/namespace.ts
+++ b/lib/routes/hackmd/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'HackMD',
url: 'hackmd.io',
+ lang: 'en',
};
diff --git a/lib/routes/hackyournews/namespace.ts b/lib/routes/hackyournews/namespace.ts
index ea4ca5b058d2bc..96eb90cfad846a 100644
--- a/lib/routes/hackyournews/namespace.ts
+++ b/lib/routes/hackyournews/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'HackYourNews',
url: 'hackyournews.com',
+ lang: 'en',
};
diff --git a/lib/routes/hafu/namespace.ts b/lib/routes/hafu/namespace.ts
index a6e45e1a9be986..aeb93e55fc0a76 100644
--- a/lib/routes/hafu/namespace.ts
+++ b/lib/routes/hafu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '河南财政金融学院',
url: 'www.hafu.edu.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hafu/news.ts b/lib/routes/hafu/news.ts
index a4f4cccfb4224f..cbc6533ebab4db 100644
--- a/lib/routes/hafu/news.ts
+++ b/lib/routes/hafu/news.ts
@@ -18,8 +18,8 @@ export const route: Route = {
maintainers: [],
handler,
description: `| 校内公告通知 | 教务处公告通知 | 招生就业处公告通知 |
- | ------------ | -------------- | ------------------ |
- | ggtz | jwc | zsjyc |`,
+| ------------ | -------------- | ------------------ |
+| ggtz | jwc | zsjyc |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hafu/utils.ts b/lib/routes/hafu/utils.ts
index c8ee1bedd63251..b1521abe3c8370 100644
--- a/lib/routes/hafu/utils.ts
+++ b/lib/routes/hafu/utils.ts
@@ -101,7 +101,7 @@ async function ggtzParse(ctx, $) {
const { articleData, description } = await tryGetFullText(href, link, 'ggtz');
let author = '';
let pubDate = '';
- if (articleData instanceof Function) {
+ if (typeof articleData === 'function') {
const header = articleData('h1').next().text();
const index = header.indexOf('日期');
@@ -148,7 +148,7 @@ async function jwcParse(ctx, $) {
const { articleData, description } = await tryGetFullText(href, link, 'jwc');
let author = '';
- if (articleData instanceof Function) {
+ if (typeof articleData === 'function') {
author = articleData('span[class=authorstyle259690]').text();
}
@@ -184,7 +184,7 @@ async function zsjycParse(ctx, $) {
const { articleData, description } = await tryGetFullText(href, link, 'zsjyc');
let pubDate = '';
- if (articleData instanceof Function) {
+ if (typeof articleData === 'function') {
const date = articleData('span[class=timestyle127702]').text();
pubDate = parseDate(date, 'YYYY-MM-DD HH:mm');
} else {
diff --git a/lib/routes/hakkatv/namespace.ts b/lib/routes/hakkatv/namespace.ts
index d6e22574776bf8..62db2b02499265 100644
--- a/lib/routes/hakkatv/namespace.ts
+++ b/lib/routes/hakkatv/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '客家電視台',
url: 'hakkatv.org.tw',
+ lang: 'zh-TW',
};
diff --git a/lib/routes/hakkatv/type.ts b/lib/routes/hakkatv/type.ts
index 0309c64d97498a..7644ad1263fe58 100644
--- a/lib/routes/hakkatv/type.ts
+++ b/lib/routes/hakkatv/type.ts
@@ -32,8 +32,8 @@ export const route: Route = {
handler,
url: 'hakkatv.org.tw/news',
description: `| 客家焦點 | 政經要聞 | 民生醫療 | 地方風采 | 國際萬象 |
- | -------- | --------- | -------- | -------- | ------------- |
- | hakka | political | medical | local | international |`,
+| -------- | --------- | -------- | -------- | ------------- |
+| hakka | political | medical | local | international |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hamel/index.ts b/lib/routes/hamel/index.ts
new file mode 100644
index 00000000000000..3946e48cba3336
--- /dev/null
+++ b/lib/routes/hamel/index.ts
@@ -0,0 +1,81 @@
+import { Route, DataItem } from '@/types';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+import cache from '@/utils/cache';
+
+export const route: Route = {
+ path: '/blog',
+ categories: ['blog'],
+ example: '/hamel/blog',
+ radar: [
+ {
+ source: ['hamel.dev/'],
+ },
+ ],
+ url: 'hamel.dev/',
+ name: 'Blog',
+ maintainers: ['liyaozhong'],
+ handler,
+ description: "Hamel's Blog Posts",
+};
+
+async function handler() {
+ const rootUrl = 'https://hamel.dev';
+ const currentUrl = rootUrl;
+
+ const response = await got(currentUrl);
+ const $ = load(response.data);
+
+ let items = $('tr[data-index]')
+ .toArray()
+ .map((item) => {
+ const $item = $(item);
+ const $link = $item.find('td a').last();
+ const $date = $item.find('.listing-date');
+
+ const href = $link.attr('href');
+ const title = $link.text().trim();
+ const dateStr = $date.text().trim();
+
+ if (!href || !title || !dateStr) {
+ return null;
+ }
+
+ const link = new URL(href, rootUrl).href;
+ const pubDate = parseDate(dateStr, 'M/D/YY');
+
+ return {
+ title,
+ link,
+ pubDate,
+ } as DataItem;
+ })
+ .filter((item): item is DataItem => item !== null);
+
+ items = (
+ await Promise.all(
+ items.map((item) =>
+ cache.tryGet(item.link as string, async () => {
+ try {
+ const detailResponse = await got(item.link);
+ const $detail = load(detailResponse.data);
+
+ return {
+ ...item,
+ description: $detail('.content').html() || '',
+ } as DataItem;
+ } catch {
+ return item;
+ }
+ })
+ )
+ )
+ ).filter((item): item is DataItem => item !== null);
+
+ return {
+ title: "Hamel's Blog",
+ link: rootUrl,
+ item: items,
+ };
+}
diff --git a/lib/routes/hamel/namespace.ts b/lib/routes/hamel/namespace.ts
new file mode 100644
index 00000000000000..f5a48a8f04c6fb
--- /dev/null
+++ b/lib/routes/hamel/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: "Hamel's Blog",
+ url: 'hamel.dev',
+ lang: 'en',
+};
diff --git a/lib/routes/hameln/chapter.ts b/lib/routes/hameln/chapter.ts
index ba040ecbaee401..678c67bdbda980 100644
--- a/lib/routes/hameln/chapter.ts
+++ b/lib/routes/hameln/chapter.ts
@@ -39,7 +39,8 @@ async function handler(ctx) {
const description = $('div.ss:nth-child(2)').text();
const chapter_list = $('tr[bgcolor]')
- .map((_, chapter) => {
+ .toArray()
+ .map((chapter) => {
const $_chapter = $(chapter);
const chapter_link = $_chapter.find('a');
return {
@@ -48,7 +49,6 @@ async function handler(ctx) {
pubDate: timezone(parseDate($_chapter.find('nobr').text(), 'YYYYMMDD HH:mm'), +9),
};
})
- .toArray()
.sort((a, b) => (a.pubDate <= b.pubDate ? 1 : -1))
.slice(0, limit);
diff --git a/lib/routes/hameln/namespace.ts b/lib/routes/hameln/namespace.ts
index b9c3ad1ea1ef8d..8cce21f5b6da91 100644
--- a/lib/routes/hameln/namespace.ts
+++ b/lib/routes/hameln/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'hameln',
url: 'syosetu.org',
+ lang: 'ja',
};
diff --git a/lib/routes/harvard/health/blog.ts b/lib/routes/harvard/health/blog.ts
index 80b11b5d3fa9b5..fa551104b569f6 100644
--- a/lib/routes/harvard/health/blog.ts
+++ b/lib/routes/harvard/health/blog.ts
@@ -6,7 +6,7 @@ import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/health/blog',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/harvard/health/blog',
parameters: {},
features: {
@@ -39,7 +39,7 @@ async function handler() {
const $ = load(response.data);
- const list = $('.lg\\:text-2xl')
+ const list = $(String.raw`.lg\:text-2xl`)
.toArray()
.map((item) => {
item = $(item).parent();
diff --git a/lib/routes/harvard/namespace.ts b/lib/routes/harvard/namespace.ts
index 7442e99dd60d90..1b114c25670185 100644
--- a/lib/routes/harvard/namespace.ts
+++ b/lib/routes/harvard/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Harvard Health Publishing',
url: 'www.health.harvard.edu',
+ lang: 'en',
};
diff --git a/lib/routes/hashnode/blog.ts b/lib/routes/hashnode/blog.ts
index 6d4d32ac56ae94..ac5aed8baff9b5 100644
--- a/lib/routes/hashnode/blog.ts
+++ b/lib/routes/hashnode/blog.ts
@@ -31,9 +31,9 @@ export const route: Route = {
maintainers: ['hnrainll'],
handler,
url: 'hashnode.dev/',
- description: `:::tip
+ description: `::: tip
username 为博主用户名,而非\`xxx.hashnode.dev\`中\`xxx\`所代表的 blog 地址。
- :::`,
+:::`,
};
async function handler(ctx) {
diff --git a/lib/routes/hashnode/namespace.ts b/lib/routes/hashnode/namespace.ts
index eebedd1f91e170..160784653f82bb 100644
--- a/lib/routes/hashnode/namespace.ts
+++ b/lib/routes/hashnode/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'hashnode',
url: 'hashnode.dev',
+ lang: 'en',
};
diff --git a/lib/routes/hbooker/namespace.ts b/lib/routes/hbooker/namespace.ts
index 340a244c6640d8..eabbe851b4c0b3 100644
--- a/lib/routes/hbooker/namespace.ts
+++ b/lib/routes/hbooker/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '欢乐书客',
url: 'hbooker.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hbr/namespace.ts b/lib/routes/hbr/namespace.ts
index d35faf28954146..1468821b0e818e 100644
--- a/lib/routes/hbr/namespace.ts
+++ b/lib/routes/hbr/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Harvard Business Review',
url: 'hbr.org',
+ lang: 'en',
};
diff --git a/lib/routes/hbr/topic.ts b/lib/routes/hbr/topic.ts
index d7bac1912aea4d..b40c2bcd276014 100644
--- a/lib/routes/hbr/topic.ts
+++ b/lib/routes/hbr/topic.ts
@@ -1,14 +1,25 @@
import { Route } from '@/types';
import cache from '@/utils/cache';
-import got from '@/utils/got';
+import ofetch from '@/utils/ofetch';
import { load } from 'cheerio';
import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/topic/:topic?/:type?',
- categories: ['new-media'],
- example: '/hbr/topic/leadership',
- parameters: { topic: 'Topic, can be found in URL, Leadership by default', type: 'Type, see below, Latest by default' },
+ categories: ['new-media', 'popular'],
+ example: '/hbr/topic/Leadership/Popular',
+ parameters: {
+ topic: 'Topic, can be found in URL, Leadership by default',
+ type: {
+ description: 'Type, see below, Popular by default',
+ options: [
+ { value: 'Popular', label: 'Popular' },
+ { value: 'From the Store', label: 'From the Store' },
+ { value: 'For You', label: 'For You' },
+ ],
+ default: 'Popular',
+ },
+ },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -23,30 +34,27 @@ export const route: Route = {
},
],
name: 'Topic',
- maintainers: ['nczitzk'],
+ maintainers: ['nczitzk', 'pseudoyu'],
handler,
- description: `| LATEST | POPULAR | FROM THE STORE | FOR YOU |
- | ------ | ------- | -------------- | ------- |
- | Latest | Popular | From the Store | For You |
+ description: `| POPULAR | FROM THE STORE | FOR YOU |
+| ------- | -------------- | ------- |
+| Popular | From the Store | For You |
- :::tip
+::: tip
Click here to view [All Topics](https://hbr.org/topics)
- :::`,
+:::`,
};
async function handler(ctx) {
- const topic = ctx.req.param('topic') ?? 'leadership';
- const type = ctx.req.param('type') ?? 'Latest';
+ const topic = ctx.req.param('topic') ?? 'Leadership';
+ const type = ctx.req.param('type') ?? 'Popular';
const rootUrl = 'https://hbr.org';
const currentUrl = `${rootUrl}/topic/${topic}`;
- const response = await got({
- method: 'get',
- url: currentUrl,
- });
+ const response = await ofetch(currentUrl);
- const $ = load(response.data);
+ const $ = load(response);
const list = $(`stream-content[data-stream-name="${type}"]`)
.find('.stream-item')
@@ -65,12 +73,9 @@ async function handler(ctx) {
const items = await Promise.all(
list.map((item) =>
cache.tryGet(item.link, async () => {
- const detailResponse = await got({
- method: 'get',
- url: item.link,
- });
+ const detailResponse = await ofetch(item.link);
- const content = load(detailResponse.data);
+ const content = load(detailResponse);
item.description = content('.article-body, article[itemprop="description"]').html();
item.pubDate = parseDate(content('meta[property="article:published_time"]').attr('content'));
diff --git a/lib/routes/hdu/auto/notice.ts b/lib/routes/hdu/auto/notice.ts
new file mode 100644
index 00000000000000..c28e87beef42bb
--- /dev/null
+++ b/lib/routes/hdu/auto/notice.ts
@@ -0,0 +1,68 @@
+import { Route } from '@/types';
+import { fetchAutoNews } from './utils';
+import logger from '@/utils/logger';
+
+const typeMap = {
+ notice: {
+ name: '通知公告',
+ path: '3779/list.htm',
+ },
+ graduate: {
+ name: '研究生教育',
+ path: '3754/list.htm',
+ },
+ undergraduate: {
+ name: '本科教学',
+ path: '3745/list.htm',
+ },
+ student: {
+ name: '学生工作',
+ path: '3726/list.htm',
+ },
+};
+
+export const route: Route = {
+ path: '/auto/:type?',
+ categories: ['university'],
+ example: '/hdu/auto',
+ parameters: { type: '分类,见下表,默认为通知公告' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ name: '自动化学院',
+ maintainers: ['jalenzz'],
+ handler: (ctx) => {
+ let type = ctx.req.param('type') || 'notice';
+ if (!(type in typeMap)) {
+ logger.error(`Invalid type: ${type}. Valid types are: ${Object.keys(typeMap).join(', ')}, defaulting to notice`);
+ type = 'notice';
+ }
+ return fetchAutoNews(typeMap[type].path, typeMap[type].name);
+ },
+ description: `| 通知公告 | 研究生教育 | 本科教学 | 学生工作 |
+| -------- | -------- | -------- | -------- |
+| notice | graduate | undergraduate | student |`,
+ radar: [
+ {
+ source: ['auto.hdu.edu.cn/main.htm', 'auto.hdu.edu.cn/3779/list.htm'],
+ target: '/auto/notice',
+ },
+ {
+ source: ['auto.hdu.edu.cn/main.htm', 'auto.hdu.edu.cn/3754/list.htm'],
+ target: '/auto/graduate',
+ },
+ {
+ source: ['auto.hdu.edu.cn/main.htm', 'auto.hdu.edu.cn/3745/list.htm'],
+ target: '/auto/undergraduate',
+ },
+ {
+ source: ['auto.hdu.edu.cn/main.htm', 'auto.hdu.edu.cn/3726/list.htm'],
+ target: '/auto/student',
+ },
+ ],
+};
diff --git a/lib/routes/hdu/auto/utils.ts b/lib/routes/hdu/auto/utils.ts
new file mode 100644
index 00000000000000..7dac71a24edca7
--- /dev/null
+++ b/lib/routes/hdu/auto/utils.ts
@@ -0,0 +1,51 @@
+import { Data, DataItem } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+
+const BASE_URL = 'https://auto.hdu.edu.cn';
+
+export const fetchAutoNews = async (path: string, title: string): Promise => {
+ const link = `${BASE_URL}/${path}`;
+ const response = await got(link);
+ const $ = load(response.data);
+
+ const list = $('.rightlist')
+ .toArray()
+ .map((item): DataItem => {
+ const $item = $(item);
+ const $a = $item.find('.newstitle a');
+ const href = $a.attr('href');
+ const title = $a.text().trim();
+ const dateMatch = $item
+ .find('.newsinfo')
+ .text()
+ .match(/日期:(\d{4}\/\d{2}\/\d{2})/);
+ const brief = $item.find('.newsbrief').text().trim();
+
+ return {
+ title: title || '无标题',
+ link: href ? new URL(href, BASE_URL).href : BASE_URL,
+ pubDate: dateMatch ? parseDate(dateMatch[1], 'YYYY/MM/DD') : undefined,
+ description: brief || '',
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const { data } = await got(item.link);
+ const $detail = load(data);
+ const description = $detail('.wp_articlecontent').html();
+ return { ...item, description: description || item.description };
+ })
+ )
+ );
+
+ return {
+ title: `杭州电子科技大学自动化学院 - ${title}`,
+ link,
+ item: items as DataItem[],
+ };
+};
diff --git a/lib/routes/hdu/namespace.ts b/lib/routes/hdu/namespace.ts
index 6d7a81aec3f603..65eb985cc05b45 100644
--- a/lib/routes/hdu/namespace.ts
+++ b/lib/routes/hdu/namespace.ts
@@ -2,5 +2,6 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '杭州电子科技大学',
- url: 'computer.hdu.edu.cn',
+ url: 'hdu.edu.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/healthz.ts b/lib/routes/healthz.ts
new file mode 100644
index 00000000000000..96f15744fadce6
--- /dev/null
+++ b/lib/routes/healthz.ts
@@ -0,0 +1,8 @@
+import type { Handler } from 'hono';
+
+const handler: Handler = (ctx) => {
+ ctx.header('Cache-Control', 'no-cache');
+ return ctx.text('ok');
+};
+
+export default handler;
diff --git a/lib/routes/hebtv/namespace.ts b/lib/routes/hebtv/namespace.ts
index 28188ddc0e5641..8423f6d51ef348 100644
--- a/lib/routes/hebtv/namespace.ts
+++ b/lib/routes/hebtv/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '河北网络广播电视台',
url: 'web.cmc.hebtv.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hellobtc/information.ts b/lib/routes/hellobtc/information.ts
index dc3c7dfde9f3af..50b424ba333310 100644
--- a/lib/routes/hellobtc/information.ts
+++ b/lib/routes/hellobtc/information.ts
@@ -19,7 +19,7 @@ const titleMap = {
export const route: Route = {
path: '/information/:channel?',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/hellobtc/information/latest',
parameters: { channel: '类型,可填 `latest` 和 `application` 及最新和应用,默认为最新' },
features: {
diff --git a/lib/routes/hellobtc/kepu.ts b/lib/routes/hellobtc/kepu.ts
index 7d8cf7728f25f1..3f71ce418716fe 100644
--- a/lib/routes/hellobtc/kepu.ts
+++ b/lib/routes/hellobtc/kepu.ts
@@ -31,7 +31,7 @@ const titleMap = {
export const route: Route = {
path: '/kepu/:channel?',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/hellobtc/kepu/latest',
parameters: { channel: '类型,见下表,默认为最新' },
features: {
@@ -46,8 +46,8 @@ export const route: Route = {
maintainers: ['Fatpandac'],
handler,
description: `| latest | bitcoin | ethereum | defi | inter\_blockchain | mining | safety | satoshi\_nakomoto | public\_blockchain |
- | ------ | ------- | -------- | ---- | ----------------- | ------ | ------ | ----------------- | ------------------ |
- | 最新 | 比特币 | 以太坊 | DeFi | 跨链 | 挖矿 | 安全 | 中本聪 | 公链 |`,
+| ------ | ------- | -------- | ---- | ----------------- | ------ | ------ | ----------------- | ------------------ |
+| 最新 | 比特币 | 以太坊 | DeFi | 跨链 | 挖矿 | 安全 | 中本聪 | 公链 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hellobtc/namespace.ts b/lib/routes/hellobtc/namespace.ts
index c6413f393b4f9a..7c72c511f537cb 100644
--- a/lib/routes/hellobtc/namespace.ts
+++ b/lib/routes/hellobtc/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '白话区块链',
url: 'hellobtc.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hellobtc/news.ts b/lib/routes/hellobtc/news.ts
index be0b1b68dc602e..4e7d0646eb3bdf 100644
--- a/lib/routes/hellobtc/news.ts
+++ b/lib/routes/hellobtc/news.ts
@@ -8,7 +8,7 @@ const rootUrl = 'https://www.hellobtc.com';
export const route: Route = {
path: '/news',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/hellobtc/news',
parameters: {},
features: {
diff --git a/lib/routes/hellogithub/article.ts b/lib/routes/hellogithub/article.ts
new file mode 100644
index 00000000000000..7e6329f6ea3d7f
--- /dev/null
+++ b/lib/routes/hellogithub/article.ts
@@ -0,0 +1,62 @@
+import { Route } from '@/types';
+
+import got from '@/utils/got';
+import { parseDate } from '@/utils/parse-date';
+
+const sorts = {
+ hot: '热门',
+ last: '最近',
+};
+
+export const route: Route = {
+ path: '/article/:sort?',
+ categories: ['programming'],
+ example: '/hellogithub/article',
+ parameters: { sort: '排序方式,见下表,默认为 `last`,即最近' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ name: '文章',
+ maintainers: ['moke8', 'nczitzk', 'CaoMeiYouRen'],
+ handler,
+ description: `| 热门 | 最近 |
+| ---- | ---- |
+| hot | last |`,
+};
+
+async function handler(ctx) {
+ const sort = ctx.req.param('sort') ?? 'last';
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20;
+
+ const rootUrl = 'https://hellogithub.com';
+ const apiRootUrl = 'https://api.hellogithub.com/v1/article/';
+ const currentUrl = `${rootUrl}/article/?sort_by=${sort}`;
+ const apiUrl = `${apiRootUrl}?sort_by=${sort}&page=1`;
+
+ const response = await got({
+ method: 'get',
+ url: apiUrl,
+ });
+
+ const items = response.data.data.slice(0, limit).map((item) => ({
+ title: item.title,
+ description: `
+
${item.desc}`,
+ link: `${rootUrl}/article/${item.aid}`,
+ author: item.author,
+ guid: item.aid,
+ pubDate: parseDate(item.publish_at),
+ }));
+
+ return {
+ title: `HelloGithub - ${sorts[sort]}文章`,
+ link: currentUrl,
+ item: items,
+ };
+}
diff --git a/lib/routes/hellogithub/index.ts b/lib/routes/hellogithub/index.ts
index c5093e5e3524fe..713f38d5b9a6e1 100644
--- a/lib/routes/hellogithub/index.ts
+++ b/lib/routes/hellogithub/index.ts
@@ -1,24 +1,19 @@
import { Route } from '@/types';
-import { getCurrentPath } from '@/utils/helpers';
-const __dirname = getCurrentPath(import.meta.url);
-
-import cache from '@/utils/cache';
import got from '@/utils/got';
+
import { load } from 'cheerio';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
-import path from 'node:path';
const sorts = {
- hot: '热门',
- last: '最近',
+ featured: '精选',
+ all: '全部',
};
export const route: Route = {
- path: ['/article/:sort?/:id?'],
+ path: '/home/:sort?/:id?',
categories: ['programming'],
- example: '/hellogithub/article',
- parameters: { sort: '排序方式,见下表,默认为 `hot`,即热门', id: '标签 id,可在对应标签页 URL 中找到,默认为全部标签' },
+ example: '/hellogithub/home',
+ parameters: { sort: '排序方式,见下表,默认为 `featured`,即精选', id: '标签 id,可在对应标签页 URL 中找到,默认为全部标签' },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -27,16 +22,16 @@ export const route: Route = {
supportPodcast: false,
supportScihub: false,
},
- name: '文章',
- maintainers: ['moke8', 'nczitzk'],
+ name: '开源项目',
+ maintainers: ['moke8', 'nczitzk', 'CaoMeiYouRen'],
handler,
- description: `| 热门 | 最近 |
- | ---- | ---- |
- | hot | last |`,
+ description: `| 精选 | 全部 |
+| ---- | ---- |
+| featured | all |`,
};
async function handler(ctx) {
- const sort = ctx.req.param('sort') ?? 'hot';
+ const sort = ctx.req.param('sort') ?? 'featured';
const id = ctx.req.param('id') ?? '';
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 20;
@@ -50,7 +45,7 @@ async function handler(ctx) {
url: apiUrl,
});
- let buildId, tag;
+ let tag;
if (id) {
const tagUrl = `${rootUrl}/tags/${id}`;
@@ -62,66 +57,21 @@ async function handler(ctx) {
const $ = load(tagResponse.data);
tag = $('meta[property="og:title"]')?.attr('content')?.split(' ').pop();
- buildId = tagResponse.data.match(/"buildId":"(.*?)",/)[1];
- }
-
- if (!buildId) {
- const buildResponse = await got({
- method: 'get',
- url: rootUrl,
- });
-
- buildId = buildResponse.data.match(/"buildId":"(.*?)",/)[1];
}
- let items = response.data.data.slice(0, limit).map((item) => ({
+ const items = response.data.data.slice(0, limit).map((item) => ({
guid: item.item_id,
- title: item.title,
+ title: `${item.name}: ${item.title}`,
author: item.author,
link: `${rootUrl}/repository/${item.item_id}`,
- description: item.description,
pubDate: parseDate(item.updated_at),
+ name: `${item.author}/${item.name}`,
+ summary: item.summary,
+ language: item.primary_lang,
}));
- items = await Promise.all(
- items.map((item) =>
- cache.tryGet(item.link, async () => {
- const detailUrl = `${rootUrl}/_next/data/${buildId}/repository/${item.guid}.json`;
-
- const detailResponse = await got({
- method: 'get',
- url: detailUrl,
- });
-
- const data = detailResponse.data.pageProps.repo;
-
- item.title = `${data.name}: ${data.title}`;
- item.category = [`No.${data.volume_name}`, ...data.tags.map((t) => t.name)];
- item.description = art(path.join(__dirname, 'templates/description.art'), {
- name: data.full_name,
- description: data.description,
- summary: data.summary,
- image: data.image_url,
- stars: data.stars ?? data.stars_str,
- isChinese: data.has_chinese,
- language: data.primary_lang,
- isActive: data.is_active,
- license: data.license,
- isOrganization: data.is_org,
- forks: data.forks,
- openIssues: data.open_issues,
- subscribers: data.subscribers,
- homepage: data.homepage,
- url: data.url,
- });
-
- return item;
- })
- )
- );
-
return {
- title: `HelloGithub - ${sorts[sort]}${tag || ''}项目`,
+ title: `HelloGithub - ${sorts[sort]}${tag || ''}开源项目`,
link: currentUrl,
item: items,
};
diff --git a/lib/routes/hellogithub/namespace.ts b/lib/routes/hellogithub/namespace.ts
index e10d95fd94f184..398b2d3e63f966 100644
--- a/lib/routes/hellogithub/namespace.ts
+++ b/lib/routes/hellogithub/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'HelloGitHub',
url: 'hellogithub.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hellogithub/report.ts b/lib/routes/hellogithub/report.ts
index 734e98e3b8bdc9..d639de80eed1c1 100644
--- a/lib/routes/hellogithub/report.ts
+++ b/lib/routes/hellogithub/report.ts
@@ -14,20 +14,20 @@ const types = {
};
export const route: Route = {
- path: ['/ranking/:type?', '/report/:type?'],
+ path: '/ranking/:type?',
example: '/hellogithub/ranking',
name: '榜单报告',
maintainers: ['moke8', 'nczitzk'],
handler,
description: `| 编程语言 | 服务器 | 数据库 |
- | -------- | -------- | ---------- |
- | tiobe | netcraft | db-engines |`,
+| -------- | -------- | ---------- |
+| tiobe | netcraft | db-engines |`,
};
async function handler(ctx) {
let type = ctx.req.param('type') ?? 'tiobe';
- type = type === 'webserver' ? 'netcraft' : type === 'db' ? 'db-engines' : type;
+ type = type === 'webserver' ? 'netcraft' : (type === 'db' ? 'db-engines' : type);
const rootUrl = 'https://hellogithub.com';
const currentUrl = `${rootUrl}/report/${type}`;
@@ -39,7 +39,7 @@ async function handler(ctx) {
const buildId = buildResponse.data.match(/"buildId":"(.*?)",/)[1];
- const apiUrl = `${rootUrl}/_next/data/${buildId}/report/${type}.json`;
+ const apiUrl = `${rootUrl}/_next/data/${buildId}/zh/report/${type}.json`;
const response = await got({
method: 'get',
diff --git a/lib/routes/hellogithub/volume.ts b/lib/routes/hellogithub/volume.ts
index 34bd20a219b2b0..5e1f41b9b174cb 100644
--- a/lib/routes/hellogithub/volume.ts
+++ b/lib/routes/hellogithub/volume.ts
@@ -12,13 +12,14 @@ const md = MarkdownIt({
import { load } from 'cheerio';
import cache from '@/utils/cache';
import { config } from '@/config';
+import { parseDate } from '@/utils/parse-date';
art.defaults.imports.render = function (string) {
return md.render(string);
};
export const route: Route = {
- path: ['/month', '/volume'],
+ path: '/volume',
example: '/hellogithub/volume',
name: '月刊',
maintainers: ['moke8', 'nczitzk', 'CaoMeiYouRen'],
@@ -39,6 +40,7 @@ async function handler(ctx) {
const items = await Promise.all(
volumes.map(async (volume) => {
const current = volume.num;
+ const lastmod = volume.lastmod;
const currentUrl = `${rootUrl}/periodical/volume/${current}`;
const key = `hellogithub:${currentUrl}`;
return await cache.tryGet(
@@ -61,6 +63,7 @@ async function handler(ctx) {
description: art(path.join(__dirname, 'templates/volume.art'), {
data: data.pageProps.volume.data,
}),
+ pubDate: parseDate(lastmod),
};
},
config.cache.routeExpire,
diff --git a/lib/routes/hex-rays/index.ts b/lib/routes/hex-rays/index.ts
index b2dd027233dfa3..3644c57ede9928 100644
--- a/lib/routes/hex-rays/index.ts
+++ b/lib/routes/hex-rays/index.ts
@@ -1,4 +1,4 @@
-import { Route } from '@/types';
+import type { Data, DataItem, Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
@@ -23,51 +23,45 @@ export const route: Route = {
},
],
name: 'Hex-Rays News',
- maintainers: ['hellodword ', 'TonyRL'],
+ maintainers: ['hellodword ', 'TonyRL', 'Mas0n'],
handler,
url: 'hex-rays.com/',
};
-async function handler() {
- const link = 'https://www.hex-rays.com/blog/';
+async function handler(/* ctx*/): Promise {
+ const link = 'https://hex-rays.com/blog/';
const response = await got.get(link);
const $ = load(response.data);
- const list = $('.post-list-container')
- .map((_, ele) => ({
- title: $('h3 > a', ele).text(),
- link: $('h3 > a', ele).attr('href'),
- pubDate: parseDate($('.post-meta:nth-of-type(1)', ele).first().text().trim().replace('Posted on:', '')),
- author: $('.post-meta:nth-of-type(2)', ele).first().text().replace('By:', '').trim(),
- }))
- .get();
+ const list: DataItem[] = $('.article ')
+ .toArray()
+ .map(
+ (ele): DataItem => ({
+ title: $('h2 > a', ele).text(),
+ link: $('h2 > a', ele).attr('href'),
+ pubDate: parseDate($('div.by-line > time', ele).attr('datetime')!),
+ author: $('div.by-line > a', ele).text(),
+ })
+ );
- const items = await Promise.all(
- list.map((item) =>
- cache.tryGet(item.link, async () => {
+ const items: DataItem[] = await Promise.all(
+ list.map((item: DataItem) =>
+ cache.tryGet(item.link!, async () => {
const detailResponse = await got.get(item.link);
const content = load(detailResponse.data);
-
- item.category = (
- content('.category-link')
- .toArray()
- .map((e) => $(e).text()) +
- ',' +
- content('.tag-link')
- .toArray()
- .map((e) => $(e).text())
- ).split(',');
-
- item.description = content('.post-content').html();
-
+ item.category = content('.div.topics > a')
+ .toArray()
+ .map((ele) => content(ele).text());
+ item.description = content('.post-body').toString();
return item;
})
- )
+ ) as PromiseAbout {{ company_name }}
+{{@ company_info_description }}
+{{/if}}
diff --git a/lib/routes/hit/namespace.ts b/lib/routes/hit/namespace.ts
index e4eae82546d766..879ee99fc8ac7f 100644
--- a/lib/routes/hit/namespace.ts
+++ b/lib/routes/hit/namespace.ts
@@ -3,7 +3,8 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '哈尔滨工业大学',
url: 'jwc.hit.edu.cn',
- description: `:::warning
+ description: `::: warning
哈工大网站疑似禁止了\`rsshub.app\`的访问,使用路由需要自行 [部署](https://docs.rsshub.app/deploy/)。
:::`,
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hit/today.ts b/lib/routes/hit/today.ts
index f3544c99a3513f..62c8aedcae3673 100644
--- a/lib/routes/hit/today.ts
+++ b/lib/routes/hit/today.ts
@@ -26,14 +26,14 @@ export const route: Route = {
name: '今日哈工大',
maintainers: ['ranpox'],
handler,
- description: `:::tip
+ description: `::: tip
今日哈工大的文章分为公告公示和新闻快讯,每个页面右侧列出了更详细的分类,其编号为每个 URL 路径的最后一个数字。
例如会议讲座的路径为\`/taxonomy/term/10/25\`,则可以通过 [\`/hit/today/25\`](https://rsshub.app/hit/today/25) 订阅该详细类别。
- :::
+:::
- :::warning
+::: warning
部分文章需要经过统一身份认证后才能阅读全文。
- :::`,
+:::`,
};
async function handler(ctx) {
diff --git a/lib/routes/hitcon/namespace.ts b/lib/routes/hitcon/namespace.ts
index 3cda88d4c2b9a1..e068a86071ce50 100644
--- a/lib/routes/hitcon/namespace.ts
+++ b/lib/routes/hitcon/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'HITCON',
url: 'hitcon.org',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hitcon/zeroday.ts b/lib/routes/hitcon/zeroday.ts
index ce3aa2a297a224..5cbd771a20f2b7 100644
--- a/lib/routes/hitcon/zeroday.ts
+++ b/lib/routes/hitcon/zeroday.ts
@@ -29,8 +29,8 @@ export const route: Route = {
},
handler,
description: `| 缺省 | all | closed | disclosed | patching |
- | ------ | ---- | ------ | --------- | -------- |
- | 活動中 | 全部 | 關閉 | 公開 | 修補中 |`,
+| ------ | ---- | ------ | --------- | -------- |
+| 活動中 | 全部 | 關閉 | 公開 | 修補中 |`,
};
const baseUrl = 'https://zeroday.hitcon.org/vulnerability';
@@ -63,7 +63,7 @@ async function handler(ctx: Context): Promise {
});
const response = await page.evaluate(() => document.documentElement.innerHTML);
- browser.close();
+ await browser.close();
const $ = load(response);
const items: DataItem[] = $('.zdui-strip-list>li')
@@ -101,7 +101,7 @@ async function handler(ctx: Context): Promise {
});
return {
- title: status ? titleMap[status] ?? 'ZeroDay' : '活動中',
+ title: status ? (titleMap[status] ?? 'ZeroDay') : '活動中',
link: url,
item: items,
image: 'https://zeroday.hitcon.org/images/favicon/favicon.png',
diff --git a/lib/routes/hitsz/article.ts b/lib/routes/hitsz/article.ts
index 11b108a64e0727..fe8731a5ed84de 100644
--- a/lib/routes/hitsz/article.ts
+++ b/lib/routes/hitsz/article.ts
@@ -22,8 +22,8 @@ export const route: Route = {
maintainers: ['xandery-geek'],
handler,
description: `| 校区要闻 | 媒体报道 | 综合新闻 | 校园动态 | 讲座论坛 | 热点专题 | 招标信息 | 重要关注 |
- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
- | id-116 | id-80 | id-75 | id-77 | id-78 | id-79 | id-81 | id-124 |`,
+| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
+| id-116 | id-80 | id-75 | id-77 | id-78 | id-79 | id-81 | id-124 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hitsz/namespace.ts b/lib/routes/hitsz/namespace.ts
index 18d76593e1198c..7dac025c6ccad0 100644
--- a/lib/routes/hitsz/namespace.ts
+++ b/lib/routes/hitsz/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '哈尔滨工业大学(深圳)',
url: 'hitsz.edu.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hitwh/namespace.ts b/lib/routes/hitwh/namespace.ts
index bbf082121473ba..1a9049c3b8b634 100644
--- a/lib/routes/hitwh/namespace.ts
+++ b/lib/routes/hitwh/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '哈尔滨工业大学(威海)',
url: 'hitwh.edu.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hizu/index.ts b/lib/routes/hizu/index.ts
index 89b682825e1c92..3a108f6d0cbe7d 100644
--- a/lib/routes/hizu/index.ts
+++ b/lib/routes/hizu/index.ts
@@ -54,31 +54,31 @@ export const route: Route = {
handler,
url: 'hizh.cn/',
description: `| 分类 | 编号 |
- | -------- | ------------------------ |
- | 热点 | 5dd92265e4b0bf88dd8c1175 |
- | 订阅 | 5dd921a7e4b0bf88dd8c116f |
- | 学党史 | 604f1cbbe4b0cf5c2234d470 |
- | 政经 | 5dd92242e4b0bf88dd8c1174 |
- | 合作区 | 61259fd6e4b0d294f7f9786d |
- | 名记名播 | 61dfe511e4b0248b60d1c568 |
- | 大湾区 | 5dd9222ce4b0bf88dd8c1173 |
- | 网评 | 617805e4e4b037abacfd4820 |
- | TV 新闻 | 5dd9220de4b0bf88dd8c1172 |
- | 音频 | 5e6edd50e4b02ebde0ab061e |
- | 澳门 | 600e8ad4e4b02c3a6af6aaa8 |
- | 政务 | 600f760fe4b0e33cf6f8e68e |
- | 教育 | 5ff7c0fde4b0e2f210d05e20 |
- | 深圳 | 5fc88615e4b0e3055e693e0a |
- | 中山 | 600e8a93e4b02c3a6af6aa80 |
- | 民生 | 5dd921ece4b0bf88dd8c1170 |
- | 社区 | 61148184e4b08d3215364396 |
- | 专题 | 5dd9215fe4b0bf88dd8c116b |
- | 战疫 | 5e2e5107e4b0c14b5d0e3d04 |
- | 横琴 | 5f88eaf2e4b0a27cd404e09e |
- | 香洲 | 5f86a3f5e4b09d75f99dde7d |
- | 金湾 | 5e8c42b4e4b0347c7e5836e0 |
- | 斗门 | 5ee70534e4b07b8a779a1ad6 |
- | 高新 | 607d37ade4b05c59ac2f3d40 |`,
+| -------- | ------------------------ |
+| 热点 | 5dd92265e4b0bf88dd8c1175 |
+| 订阅 | 5dd921a7e4b0bf88dd8c116f |
+| 学党史 | 604f1cbbe4b0cf5c2234d470 |
+| 政经 | 5dd92242e4b0bf88dd8c1174 |
+| 合作区 | 61259fd6e4b0d294f7f9786d |
+| 名记名播 | 61dfe511e4b0248b60d1c568 |
+| 大湾区 | 5dd9222ce4b0bf88dd8c1173 |
+| 网评 | 617805e4e4b037abacfd4820 |
+| TV 新闻 | 5dd9220de4b0bf88dd8c1172 |
+| 音频 | 5e6edd50e4b02ebde0ab061e |
+| 澳门 | 600e8ad4e4b02c3a6af6aaa8 |
+| 政务 | 600f760fe4b0e33cf6f8e68e |
+| 教育 | 5ff7c0fde4b0e2f210d05e20 |
+| 深圳 | 5fc88615e4b0e3055e693e0a |
+| 中山 | 600e8a93e4b02c3a6af6aa80 |
+| 民生 | 5dd921ece4b0bf88dd8c1170 |
+| 社区 | 61148184e4b08d3215364396 |
+| 专题 | 5dd9215fe4b0bf88dd8c116b |
+| 战疫 | 5e2e5107e4b0c14b5d0e3d04 |
+| 横琴 | 5f88eaf2e4b0a27cd404e09e |
+| 香洲 | 5f86a3f5e4b09d75f99dde7d |
+| 金湾 | 5e8c42b4e4b0347c7e5836e0 |
+| 斗门 | 5ee70534e4b07b8a779a1ad6 |
+| 高新 | 607d37ade4b05c59ac2f3d40 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hizu/namespace.ts b/lib/routes/hizu/namespace.ts
index fd72e76e887b4f..27dcffa1e41c71 100644
--- a/lib/routes/hizu/namespace.ts
+++ b/lib/routes/hizu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '珠海网',
url: 'hizh.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hk01/namespace.ts b/lib/routes/hk01/namespace.ts
index 22fa57ef1ebe0c..63e2ef205f1d31 100644
--- a/lib/routes/hk01/namespace.ts
+++ b/lib/routes/hk01/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '香港 01',
url: 'hk01.com',
+ lang: 'zh-HK',
};
diff --git a/lib/routes/hkej/index.ts b/lib/routes/hkej/index.ts
index 619b00172d72e1..08bab035e798c6 100644
--- a/lib/routes/hkej/index.ts
+++ b/lib/routes/hkej/index.ts
@@ -81,8 +81,8 @@ export const route: Route = {
handler,
url: 'hkej.com/',
description: `| index | stock | hongkong | china | international | property | current |
- | -------- | -------- | -------- | -------- | ------------- | -------- | -------- |
- | 全部新闻 | 港股直击 | 香港财经 | 中国财经 | 国际财经 | 地产新闻 | 时事脉搏 |`,
+| -------- | -------- | -------- | -------- | ------------- | -------- | -------- |
+| 全部新闻 | 港股直击 | 香港财经 | 中国财经 | 国际财经 | 地产新闻 | 时事脉搏 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hkej/namespace.ts b/lib/routes/hkej/namespace.ts
index 2d773b44fc9e8b..bde05ea0306f3e 100644
--- a/lib/routes/hkej/namespace.ts
+++ b/lib/routes/hkej/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '信报财经新闻',
url: 'hkej.com',
+ lang: 'zh-HK',
};
diff --git a/lib/routes/hkepc/index.ts b/lib/routes/hkepc/index.ts
index f64bed5c9c9bf0..d479cf3a632576 100644
--- a/lib/routes/hkepc/index.ts
+++ b/lib/routes/hkepc/index.ts
@@ -8,7 +8,7 @@ import { baseUrl, categoryMap } from './data';
export const route: Route = {
path: '/:category?',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/hkepc/news',
parameters: { category: '分类,见下表,默认为最新消息' },
features: {
@@ -30,8 +30,8 @@ export const route: Route = {
handler,
url: 'hkepc.com/',
description: `| 专题报导 | 新闻中心 | 新品快递 | 超频领域 | 流动数码 | 生活娱乐 | 会员消息 | 脑场新闻 | 业界资讯 | 最新消息 |
- | ---------- | -------- | -------- | -------- | -------- | ------------- | -------- | -------- | -------- | -------- |
- | coverStory | news | review | ocLab | digital | entertainment | member | price | press | latest |`,
+| ---------- | -------- | -------- | -------- | -------- | ------------- | -------- | -------- | -------- | -------- |
+| coverStory | news | review | ocLab | digital | entertainment | member | price | press | latest |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hkepc/namespace.ts b/lib/routes/hkepc/namespace.ts
index 9bbd2de097feeb..213680966aed64 100644
--- a/lib/routes/hkepc/namespace.ts
+++ b/lib/routes/hkepc/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'HKEPC',
url: 'hkepc.com',
+ lang: 'zh-HK',
};
diff --git a/lib/routes/hket/index.ts b/lib/routes/hket/index.ts
index 42612bc52e2772..7abfa363309025 100644
--- a/lib/routes/hket/index.ts
+++ b/lib/routes/hket/index.ts
@@ -1,10 +1,10 @@
-import { Route } from '@/types';
+import { DataItem, Route } from '@/types';
import { getCurrentPath } from '@/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);
import cache from '@/utils/cache';
-import got from '@/utils/got';
-import { load } from 'cheerio';
+import ofetch from '@/utils/ofetch';
+import * as cheerio from 'cheerio';
import { parseDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
import path from 'node:path';
@@ -39,9 +39,25 @@ export const route: Route = {
supportScihub: false,
},
radar: [
+ {
+ source: ['china.hket.com/:category/*'],
+ target: '/:category',
+ },
+ {
+ source: ['inews.hket.com/:category/*'],
+ target: '/:category',
+ },
+ {
+ source: ['topick.hket.com/:category/*'],
+ target: '/:category',
+ },
+ {
+ source: ['wealth.hket.com/:category/*'],
+ target: '/:category',
+ },
{
source: ['www.hket.com/'],
- target: '',
+ target: '/',
},
],
name: '新闻',
@@ -52,110 +68,106 @@ export const route: Route = {
此路由主要补全官方 RSS 全文输出及完善分类输出。
- 分类
+分类
- | sran001 | sran008 | sran010 | sran011 | sran012 | srat006 |
- | -------- | -------- | -------- | -------- | -------- | -------- |
- | 全部新闻 | 财经地产 | 科技信息 | 国际新闻 | 商业新闻 | 香港新闻 |
+| sran001 | sran008 | sran010 | sran011 | sran012 | srat006 |
+| -------- | -------- | -------- | -------- | -------- | -------- |
+| 全部新闻 | 财经地产 | 科技信息 | 国际新闻 | 商业新闻 | 香港新闻 |
- | sran009 | sran009-1 | sran009-2 | sran009-3 | sran009-4 | sran009-5 | sran009-6 |
- | -------- | --------- | --------- | ---------- | --------- | --------- | --------- |
- | 即时财经 | 股市 | 新股 IPO | 新经济追踪 | 当炒股 | 宏观解读 | Hot Talk |
+| sran009 | sran009-1 | sran009-2 | sran009-3 | sran009-4 | sran009-5 | sran009-6 |
+| -------- | --------- | --------- | ---------- | --------- | --------- | --------- |
+| 即时财经 | 股市 | 新股 IPO | 新经济追踪 | 当炒股 | 宏观解读 | Hot Talk |
- | sran011-1 | sran011-2 | sran011-3 |
- | --------- | ------------ | ------------ |
- | 环球政治 | 环球经济金融 | 环球社会热点 |
+| sran011-1 | sran011-2 | sran011-3 |
+| --------- | ------------ | ------------ |
+| 环球政治 | 环球经济金融 | 环球社会热点 |
- | sran016 | sran016-1 | sran016-2 | sran016-3 | sran016-4 | sran016-5 |
- | ---------- | ---------- | ---------- | ---------- | ---------- | -------------- |
- | 大湾区主页 | 大湾区发展 | 大湾区工作 | 大湾区买楼 | 大湾区消费 | 大湾区投资理财 |
+| sran016 | sran016-1 | sran016-2 | sran016-3 | sran016-4 | sran016-5 |
+| ---------- | ---------- | ---------- | ---------- | ---------- | -------------- |
+| 大湾区主页 | 大湾区发展 | 大湾区工作 | 大湾区买楼 | 大湾区消费 | 大湾区投资理财 |
- | srac002 | srac003 | srac004 | srac005 |
- | -------- | -------- | -------- | -------- |
- | 即时中国 | 经济脉搏 | 国情动向 | 社会热点 |
+| srac002 | srac003 | srac004 | srac005 |
+| -------- | -------- | -------- | -------- |
+| 即时中国 | 经济脉搏 | 国情动向 | 社会热点 |
- | srat001 | srat008 | srat055 | srat069 | srat070 |
- | ------- | ------- | -------- | -------- | --------- |
- | 话题 | 观点 | 休闲消费 | 娱乐新闻 | TOPick TV |
+| srat001 | srat008 | srat055 | srat069 | srat070 |
+| ------- | ------- | -------- | -------- | --------- |
+| 话题 | 观点 | 休闲消费 | 娱乐新闻 | TOPick TV |
- | srat052 | srat052-1 | srat052-2 | srat052-3 |
- | -------- | --------- | ---------- | --------- |
- | 健康主页 | 食用安全 | 医生诊症室 | 保健美颜 |
+| srat052 | srat052-1 | srat052-2 | srat052-3 |
+| -------- | --------- | ---------- | --------- |
+| 健康主页 | 食用安全 | 医生诊症室 | 保健美颜 |
- | srat053 | srat053-1 | srat053-2 | srat053-3 | srat053-4 |
- | -------- | --------- | --------- | --------- | ---------- |
- | 亲子主页 | 儿童健康 | 育儿经 | 教育 | 亲子好去处 |
+| srat053 | srat053-1 | srat053-2 | srat053-3 | srat053-4 |
+| -------- | --------- | --------- | --------- | ---------- |
+| 亲子主页 | 儿童健康 | 育儿经 | 教育 | 亲子好去处 |
- | srat053-6 | srat053-61 | srat053-62 | srat053-63 | srat053-64 |
- | ----------- | ---------- | ---------- | ---------- | ---------- |
- | Band 1 学堂 | 幼稚园 | 中小学 | 尖子教室 | 海外升学 |
+| srat053-6 | srat053-61 | srat053-62 | srat053-63 | srat053-64 |
+| ----------- | ---------- | ---------- | ---------- | ---------- |
+| Band 1 学堂 | 幼稚园 | 中小学 | 尖子教室 | 海外升学 |
- | srat072-1 | srat072-2 | srat072-3 | srat072-4 |
- | ---------- | ---------- | ---------------- | ----------------- |
- | 健康身心活 | 抗癌新方向 | 「糖」「心」解密 | 风湿不再 你我自在 |
+| srat072-1 | srat072-2 | srat072-3 | srat072-4 |
+| ---------- | ---------- | ---------------- | ----------------- |
+| 健康身心活 | 抗癌新方向 | 「糖」「心」解密 | 风湿不再 你我自在 |
- | sraw007 | sraw009 | sraw010 | sraw011 | sraw012 | sraw014 | sraw018 | sraw019 |
- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
- | 全部博客 | Bloggers | 收息攻略 | 精明消费 | 退休规划 | 个人增值 | 财富管理 | 绿色金融 |
+| sraw007 | sraw009 | sraw010 | sraw011 | sraw012 | sraw014 | sraw018 | sraw019 |
+| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
+| 全部博客 | Bloggers | 收息攻略 | 精明消费 | 退休规划 | 个人增值 | 财富管理 | 绿色金融 |
- | sraw015 | sraw015-07 | sraw015-08 | sraw015-09 | sraw015-10 |
- | -------- | ---------- | ---------- | ---------- | ---------- |
- | 移民百科 | 海外置业 | 移民攻略 | 移民点滴 | 海外理财 |
+| sraw015 | sraw015-07 | sraw015-08 | sraw015-09 | sraw015-10 |
+| -------- | ---------- | ---------- | ---------- | ---------- |
+| 移民百科 | 海外置业 | 移民攻略 | 移民点滴 | 海外理财 |
- | sraw020 | sraw020-1 | sraw020-2 | sraw020-3 | sraw020-4 |
- | -------- | ------------ | --------- | --------- | --------- |
- | ESG 主页 | ESG 趋势政策 | ESG 投资 | ESG 企业 | ESG 社会 |
-
+
` : ''}
${item.description}
Last updated: ${item.last_updated}
Stars: ${item.stargazers_count}
Topics: ${item.topics?.join(', ')}`,
+ link: `https://github.com/${item.full_name}`,
+ guid: item.domain || item.full_name,
+ tags: item.topics,
+ pubDate: new Date(item.last_fetched * 1000),
+ })),
+ };
+}
diff --git a/lib/routes/home-assistant/namespace.ts b/lib/routes/home-assistant/namespace.ts
new file mode 100644
index 00000000000000..6764c4fb2fee8d
--- /dev/null
+++ b/lib/routes/home-assistant/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Home Assistant',
+ url: 'www.home-assistant.io',
+ lang: 'en',
+};
diff --git a/lib/routes/hongkong/dh.ts b/lib/routes/hongkong/dh.ts
index a94374b1ef69c0..767e6cfed7f2b7 100644
--- a/lib/routes/hongkong/dh.ts
+++ b/lib/routes/hongkong/dh.ts
@@ -28,9 +28,9 @@ export const route: Route = {
url: 'dh.gov.hk/',
description: `Language
- | English | 中文简体 | 中文繁體 |
- | ------- | -------- | -------- |
- | english | chs | tc\_chi |`,
+| English | 中文简体 | 中文繁體 |
+| ------- | -------- | -------- |
+| english | chs | tc\_chi |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hongkong/namespace.ts b/lib/routes/hongkong/namespace.ts
index a7b5a93505c8a7..ffeacd420ac073 100644
--- a/lib/routes/hongkong/namespace.ts
+++ b/lib/routes/hongkong/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Hong Kong Department of Health 香港卫生署',
url: 'dh.gov.hk',
+ lang: 'zh-HK',
};
diff --git a/lib/routes/hostmonit/cloudflareyes.ts b/lib/routes/hostmonit/cloudflareyes.ts
index 7800b3383a7070..ff1ce007e0d8dc 100644
--- a/lib/routes/hostmonit/cloudflareyes.ts
+++ b/lib/routes/hostmonit/cloudflareyes.ts
@@ -32,8 +32,8 @@ export const route: Route = {
maintainers: ['nczitzk'],
handler,
description: `| v4 | v6 |
- | -- | -- |
- | | v6 |`,
+| -- | -- |
+| | v6 |`,
};
async function handler(ctx) {
@@ -64,7 +64,7 @@ async function handler(ctx) {
const items = response.info.slice(0, limit).map((item) => {
const ip = item.ip;
const latency = item.latency === undefined ? undefined : `${item.latency}ms`;
- const line = item.line === undefined ? undefined : Object.hasOwn(lines, item.line) ? lines[item.line] : item.line;
+ const line = item.line === undefined ? undefined : (Object.hasOwn(lines, item.line) ? lines[item.line] : item.line);
const loss = item.loss === undefined ? undefined : `${item.loss}%`;
const node = item.node;
const speed = item.speed === undefined ? undefined : `${item.speed} KB/s`;
diff --git a/lib/routes/hostmonit/cloudflareyesv6.ts b/lib/routes/hostmonit/cloudflareyesv6.ts
index a52287cfb06485..c7d38cbae38796 100644
--- a/lib/routes/hostmonit/cloudflareyesv6.ts
+++ b/lib/routes/hostmonit/cloudflareyesv6.ts
@@ -7,5 +7,5 @@ export const route: Route = {
};
function handler(ctx) {
- ctx.redirect('/hostmonit/cloudflareyes/v6');
+ ctx.set('redirect', '/hostmonit/cloudflareyes/v6');
}
diff --git a/lib/routes/hostmonit/namespace.ts b/lib/routes/hostmonit/namespace.ts
index 091f4f8548513b..251a42275a8160 100644
--- a/lib/routes/hostmonit/namespace.ts
+++ b/lib/routes/hostmonit/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '全球主机监控',
url: 'stock.hostmonit.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hottoys/index.ts b/lib/routes/hottoys/index.ts
new file mode 100644
index 00000000000000..a1cc08f840c543
--- /dev/null
+++ b/lib/routes/hottoys/index.ts
@@ -0,0 +1,63 @@
+import { Route } from '@/types';
+import { load } from 'cheerio';
+import puppeteer from '@/utils/puppeteer';
+
+export const route: Route = {
+ path: '/',
+ categories: ['shopping'],
+ example: '/hottoys',
+ radar: [
+ {
+ source: ['hottoys.com.hk/'],
+ },
+ ],
+ name: 'Toys List',
+ maintainers: ['jw0903'],
+ handler,
+ url: 'hottoys.com.hk/',
+ features: {
+ requirePuppeteer: true,
+ },
+};
+
+async function handler() {
+ const baseUrl = 'https://www.hottoys.com.hk';
+
+ // 导入 puppeteer 工具类并初始化浏览器实例
+ const browser = await puppeteer();
+ // 打开一个新标签页
+ const page = await browser.newPage();
+ // 拦截所有请求
+ await page.setRequestInterception(true);
+
+ page.on('request', (request) => {
+ // 在这次例子,我们只允许 HTML 请求
+ request.resourceType() === 'document' ? request.continue() : request.abort();
+ });
+
+ await page.goto(baseUrl, {
+ waitUntil: 'domcontentloaded',
+ });
+ const response = await page.content();
+ await page.close();
+ const $ = load(response);
+ const items = $('li.productListItem')
+ .toArray()
+ .map((item) => {
+ const dom = $(item);
+ const a = dom.find('a').first();
+ const img = dom.find('img').first();
+ return {
+ title: img.attr('title') ?? 'hottoys',
+ link: `${baseUrl}/${a.attr('href')}`,
+ description: ``,
+ guid: a.attr('href'),
+ };
+ });
+ await browser.close();
+ return {
+ title: 'Hot Toys New Products',
+ link: baseUrl,
+ item: items,
+ };
+}
diff --git a/lib/routes/hottoys/namespace.ts b/lib/routes/hottoys/namespace.ts
new file mode 100644
index 00000000000000..a314fddbce4568
--- /dev/null
+++ b/lib/routes/hottoys/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Hot Toys',
+ url: 'www.hottoys.com.hk',
+ lang: 'zh-HK',
+};
diff --git a/lib/routes/hotukdeals/namespace.ts b/lib/routes/hotukdeals/namespace.ts
index 0503c22e7e57b9..020ccf02e06f4e 100644
--- a/lib/routes/hotukdeals/namespace.ts
+++ b/lib/routes/hotukdeals/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'hotukdeals',
url: 'www.hotukdeals.com',
+ lang: 'en',
};
diff --git a/lib/routes/houxu/events.ts b/lib/routes/houxu/events.ts
index 5ac31cafc0ff2d..9e4d5770c11e54 100644
--- a/lib/routes/houxu/events.ts
+++ b/lib/routes/houxu/events.ts
@@ -11,15 +11,6 @@ export const route: Route = {
path: '/events',
categories: ['new-media'],
example: '/houxu/events',
- parameters: {},
- features: {
- requireConfig: false,
- requirePuppeteer: false,
- antiCrawler: false,
- supportBT: false,
- supportPodcast: false,
- supportScihub: false,
- },
radar: [
{
source: ['houxu.app/events', 'houxu.app/'],
diff --git a/lib/routes/houxu/index.ts b/lib/routes/houxu/index.ts
index 1b1d4c0152a343..fcac8b90faeec6 100644
--- a/lib/routes/houxu/index.ts
+++ b/lib/routes/houxu/index.ts
@@ -8,19 +8,17 @@ import { art } from '@/utils/render';
import path from 'node:path';
export const route: Route = {
- path: ['/featured', '/index', '/'],
+ name: '热点',
+ maintainers: ['nczitzk'],
+ example: '/houxu',
+ path: '/',
radar: [
{
source: ['houxu.app/'],
- target: '',
},
],
- name: 'Unknown',
- maintainers: [],
handler,
url: 'houxu.app/',
- url: 'houxu.app/',
- url: 'houxu.app/',
};
async function handler(ctx) {
diff --git a/lib/routes/houxu/memory.ts b/lib/routes/houxu/memory.ts
index 11c3c5c4344209..6e108b978be4d2 100644
--- a/lib/routes/houxu/memory.ts
+++ b/lib/routes/houxu/memory.ts
@@ -11,15 +11,6 @@ export const route: Route = {
path: '/memory',
categories: ['new-media'],
example: '/houxu/memory',
- parameters: {},
- features: {
- requireConfig: false,
- requirePuppeteer: false,
- antiCrawler: false,
- supportBT: false,
- supportPodcast: false,
- supportScihub: false,
- },
radar: [
{
source: ['houxu.app/memory', 'houxu.app/'],
diff --git a/lib/routes/houxu/namespace.ts b/lib/routes/houxu/namespace.ts
index 81921ab0c4512f..072fc504156af6 100644
--- a/lib/routes/houxu/namespace.ts
+++ b/lib/routes/houxu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '后续',
url: 'houxu.app',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/howtoforge/namespace.ts b/lib/routes/howtoforge/namespace.ts
index b421fe1f4aab3c..e45d782f9daee9 100644
--- a/lib/routes/howtoforge/namespace.ts
+++ b/lib/routes/howtoforge/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Howtoforge Linux Tutorials',
url: 'howtoforge.com',
+ lang: 'en',
};
diff --git a/lib/routes/hoyolab/namespace.ts b/lib/routes/hoyolab/namespace.ts
index 70d118562f9b9b..396c0a85ed6dcf 100644
--- a/lib/routes/hoyolab/namespace.ts
+++ b/lib/routes/hoyolab/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'HoYoLAB',
url: 'hoyolab.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hoyolab/news.ts b/lib/routes/hoyolab/news.ts
index 3a7678dd7e5bad..1411d678dda982 100644
--- a/lib/routes/hoyolab/news.ts
+++ b/lib/routes/hoyolab/news.ts
@@ -91,29 +91,29 @@ export const route: Route = {
maintainers: ['ZenoTian'],
handler,
description: `| Language | Code |
- | ---------------- | ----- |
- | 简体中文 | zh-cn |
- | 繁體中文 | zh-tw |
- | 日本語 | ja-jp |
- | 한국어 | ko-kr |
- | English (US) | en-us |
- | Español (EU) | es-es |
- | Français | fr-fr |
- | Deutsch | de-de |
- | Русский | ru-ru |
- | Português | pt-pt |
- | Español (Latino) | es-mx |
- | Indonesia | id-id |
- | Tiếng Việt | vi-vn |
- | ภาษาไทย | th-th |
+| ---------------- | ----- |
+| 简体中文 | zh-cn |
+| 繁體中文 | zh-tw |
+| 日本語 | ja-jp |
+| 한국어 | ko-kr |
+| English (US) | en-us |
+| Español (EU) | es-es |
+| Français | fr-fr |
+| Deutsch | de-de |
+| Русский | ru-ru |
+| Português | pt-pt |
+| Español (Latino) | es-mx |
+| Indonesia | id-id |
+| Tiếng Việt | vi-vn |
+| ภาษาไทย | th-th |
- | Honkai Impact 3rd | Genshin Impact | Tears of Themis | HoYoLAB | Honkai: Star Rail | Zenless Zone Zero |
- | ----------------- | -------------- | --------------- | ------- | ----------------- | ----------------- |
- | 1 | 2 | 4 | 5 | 6 | 8 |
+| Honkai Impact 3rd | Genshin Impact | Tears of Themis | HoYoLAB | Honkai: Star Rail | Zenless Zone Zero |
+| ----------------- | -------------- | --------------- | ------- | ----------------- | ----------------- |
+| 1 | 2 | 4 | 5 | 6 | 8 |
- | Notices | Events | Info |
- | ------- | ------ | ---- |
- | 1 | 2 | 3 |`,
+| Notices | Events | Info |
+| ------- | ------ | ---- |
+| 1 | 2 | 3 |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hpoi/all.ts b/lib/routes/hpoi/all.ts
index 189adb1757ae43..135abfa3a21eaa 100644
--- a/lib/routes/hpoi/all.ts
+++ b/lib/routes/hpoi/all.ts
@@ -1,11 +1,25 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import { ProcessFeed } from './utils';
export const route: Route = {
path: '/items/all/:order?',
- categories: ['anime'],
+ categories: ['anime', 'popular'],
+ view: ViewType.Pictures,
example: '/hpoi/items/all',
- parameters: { order: '排序, 见下表,默认为 add' },
+ parameters: {
+ order: {
+ description: '排序',
+ options: [
+ { value: 'release', label: '发售' },
+ { value: 'add', label: '入库' },
+ { value: 'hits', label: '总热度' },
+ { value: 'hits7Day', label: '一周热度' },
+ { value: 'hitsDay', label: '一天热度' },
+ { value: 'rating', label: '评价' },
+ ],
+ default: 'add',
+ },
+ },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -24,9 +38,6 @@ export const route: Route = {
maintainers: ['DIYgod'],
handler,
url: 'www.hpoi.net/hobby/all',
- description: `| 发售 | 入库 | 总热度 | 一周热度 | 一天热度 | 评价 |
- | ------- | ---- | ------ | -------- | -------- | ------ |
- | release | add | hits | hits7Day | hitsDay | rating |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hpoi/character.ts b/lib/routes/hpoi/character.ts
index 8dd1cfc3001ff4..ae8b705bfbb0d9 100644
--- a/lib/routes/hpoi/character.ts
+++ b/lib/routes/hpoi/character.ts
@@ -1,11 +1,26 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import { ProcessFeed } from './utils';
export const route: Route = {
path: '/items/character/:id/:order?',
- categories: ['anime'],
+ categories: ['anime', 'popular'],
+ view: ViewType.Pictures,
example: '/hpoi/items/character/1035374',
- parameters: { id: '角色 ID', order: '排序, 见下表,默认为 add' },
+ parameters: {
+ id: '角色 ID',
+ order: {
+ description: '排序',
+ options: [
+ { value: 'release', label: '发售' },
+ { value: 'add', label: '入库' },
+ { value: 'hits', label: '总热度' },
+ { value: 'hits7Day', label: '一周热度' },
+ { value: 'hitsDay', label: '一天热度' },
+ { value: 'rating', label: '评价' },
+ ],
+ default: 'add',
+ },
+ },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -17,9 +32,6 @@ export const route: Route = {
name: '角色周边',
maintainers: ['DIYgod'],
handler,
- description: `| 发售 | 入库 | 总热度 | 一周热度 | 一天热度 | 评价 |
- | ------- | ---- | ------ | -------- | -------- | ------ |
- | release | add | hits | hits7Day | hitsDay | rating |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hpoi/info.ts b/lib/routes/hpoi/info.ts
index 1550594db83cd2..6d9177f506d0fb 100644
--- a/lib/routes/hpoi/info.ts
+++ b/lib/routes/hpoi/info.ts
@@ -7,7 +7,17 @@ export const route: Route = {
path: '/info/:type?',
categories: ['anime'],
example: '/hpoi/info/all',
- parameters: { type: '分类, 见下表, 默认为`all`' },
+ parameters: {
+ type: {
+ description: '分类',
+ options: [
+ { value: 'all', label: '全部' },
+ { value: 'hobby', label: '手办' },
+ { value: 'model', label: '模型' },
+ ],
+ default: 'all',
+ },
+ },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -19,11 +29,6 @@ export const route: Route = {
name: '情报',
maintainers: ['sanmmm DIYgod'],
handler,
- description: `分类
-
- | 全部 | 手办 | 模型 |
- | ---- | ----- | ----- |
- | all | hobby | model |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hpoi/namespace.ts b/lib/routes/hpoi/namespace.ts
index def496d5857dc7..029aa6ebc39f2d 100644
--- a/lib/routes/hpoi/namespace.ts
+++ b/lib/routes/hpoi/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Hpoi 手办维基',
url: 'www.hpoi.net',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hpoi/user.ts b/lib/routes/hpoi/user.ts
index f51a0e916a243f..4f11bd557bad9a 100644
--- a/lib/routes/hpoi/user.ts
+++ b/lib/routes/hpoi/user.ts
@@ -16,7 +16,22 @@ export const route: Route = {
path: '/user/:user_id/:caty',
categories: ['anime'],
example: '/hpoi/user/116297/buy',
- parameters: { user_id: '用户ID', caty: '类别, 见下表' },
+ parameters: {
+ user_id: {
+ description: '用户ID',
+ },
+ caty: {
+ description: '类别',
+ options: [
+ { value: 'want', label: '想买' },
+ { value: 'preorder', label: '预定' },
+ { value: 'buy', label: '已入' },
+ { value: 'care', label: '关注' },
+ { value: 'resell', label: '有过' },
+ ],
+ default: 'buy',
+ },
+ },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -28,9 +43,6 @@ export const route: Route = {
name: '用户动态',
maintainers: ['DIYgod', 'luyuhuang'],
handler,
- description: `| 想买 | 预定 | 已入 | 关注 | 有过 |
- | ---- | -------- | ---- | ---- | ------ |
- | want | preorder | buy | care | resell |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hpoi/utils.ts b/lib/routes/hpoi/utils.ts
index 100d9c40816559..87561fb69cdc6b 100644
--- a/lib/routes/hpoi/utils.ts
+++ b/lib/routes/hpoi/utils.ts
@@ -12,6 +12,10 @@ const MAPs = {
url: `${host}/hobby/all?order={order}&r18=-1&works={id}`,
title: '作品周边',
},
+ overview: {
+ url: `${host}/works/{id}`,
+ title: '周边总览',
+ },
all: {
url: `${host}/hobby/all?order={order}&r18=-1`,
title: '全部周边',
@@ -19,15 +23,44 @@ const MAPs = {
};
const ProcessFeed = async (type, id, order) => {
- const link = MAPs[type].url.replace(/{id}/, id).replace(/{order}/, order || 'add');
- const response = await got({
+ let link = MAPs[type].url.replace(/{id}/, id).replace(/{order}/, order || 'add');
+ let response = await got({
method: 'get',
url: link,
headers: {
Referer: host,
},
});
- const $ = load(response.data);
+ let $ = load(response.data);
+
+ if (type === 'work') {
+ const overviewLink = MAPs.overview.url.replace(/{id}/, id);
+ const overviewResponse = await got({
+ method: 'get',
+ url: overviewLink,
+ headers: {
+ Referer: host,
+ },
+ });
+ const $overview = load(overviewResponse.data);
+
+ const moreLink = $overview('.company-ibox a.hpoi-btn-border.hpoi-btn-more').attr('href');
+ if (moreLink) {
+ const worksId = moreLink.match(/modal\/taobao\/more\/(\d+)/)?.[1];
+ if (worksId) {
+ link = `${host}/hobby/all?order=${order || 'add'}&r18=-1&works=${worksId}`;
+ response = await got({
+ method: 'get',
+ url: link,
+ headers: {
+ Referer: host,
+ },
+ });
+ $ = load(response.data);
+ }
+ }
+ }
+
return {
title: `Hpoi 手办维基 - ${MAPs[type].title}${id ? ` ${id}` : ''}`,
link,
diff --git a/lib/routes/hpoi/work.ts b/lib/routes/hpoi/work.ts
index f30dbc3204c595..7a609bb3628b73 100644
--- a/lib/routes/hpoi/work.ts
+++ b/lib/routes/hpoi/work.ts
@@ -1,11 +1,26 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import { ProcessFeed } from './utils';
export const route: Route = {
path: '/items/work/:id/:order?',
- categories: ['anime'],
+ categories: ['anime', 'popular'],
+ view: ViewType.Pictures,
example: '/hpoi/items/work/4117491',
- parameters: { id: '作品 ID', order: '排序, 见下表,默认为 add' },
+ parameters: {
+ id: '作品 ID',
+ order: {
+ description: '排序',
+ options: [
+ { value: 'release', label: '发售' },
+ { value: 'add', label: '入库' },
+ { value: 'hits', label: '总热度' },
+ { value: 'hits7Day', label: '一周热度' },
+ { value: 'hitsDay', label: '一天热度' },
+ { value: 'rating', label: '评价' },
+ ],
+ default: 'add',
+ },
+ },
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -17,9 +32,6 @@ export const route: Route = {
name: '作品周边',
maintainers: ['DIYgod'],
handler,
- description: `| 发售 | 入库 | 总热度 | 一周热度 | 一天热度 | 评价 |
- | ------- | ---- | ------ | -------- | -------- | ------ |
- | release | add | hits | hits7Day | hitsDay | rating |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hrbeu/cec/list.ts b/lib/routes/hrbeu/cec/list.ts
new file mode 100644
index 00000000000000..1c37a38cbf4fa5
--- /dev/null
+++ b/lib/routes/hrbeu/cec/list.ts
@@ -0,0 +1,82 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+const rootUrl = 'http://cec.hrbeu.edu.cn';
+
+export const route: Route = {
+ path: '/cec/:id',
+ categories: ['university'],
+ example: '/hrbeu/cec/tzgg',
+ parameters: { id: '栏目编号,由 `URL` 中获取。' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['cec.hrbeu.edu.cn/:id/list.htm'],
+ },
+ ],
+ name: '航天与建筑工程学院',
+ maintainers: ['tsinglinrain'],
+ handler,
+ description: `汉语拼音和中文不对应,猜测后三个为:教务工作、科研成果、学生工作的拼音。
+
+| 新闻动态 | 通知公告 | 综合办公 | 教务动态 | 科研动态 | 学工动态 |
+| :------: | :------: |:------: | :------: | :------: | :------: |
+| xwdt | tzgg | zhbg | jxgz | kycg | xsgz |`,
+};
+
+async function handler(ctx) {
+ const id = ctx.req.param('id');
+ const response = await got(`${rootUrl}/${id}/list.htm`, {
+ headers: {
+ Referer: rootUrl,
+ },
+ });
+
+ const $ = load(response.data);
+
+ const bigTitle = $('div.column-news-box').find('h2.column-title').text().replaceAll(/[\s·]/g, '').trim();
+
+ const list = $('a.column-news-item')
+ .toArray()
+ .map((item) => {
+ let link = $(item).attr('href');
+ if (link && link.includes('page.htm')) {
+ link = `${rootUrl}${link}`;
+ }
+ return {
+ title: $(item).find('span.column-news-title').text().trim(),
+ pubDate: parseDate($(item).find('span.column-news-date').text()),
+ link,
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ if (item.link.includes('page.htm')) {
+ const detailResponse = await got(item.link);
+ const content = load(detailResponse.data);
+ item.description = content('div.wp_articlecontent').html();
+ } else {
+ item.description = '本文需跳转,请点击标题后阅读';
+ }
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: '航天与建筑工程学院 - ' + bigTitle,
+ link: `${rootUrl}/${id}/list.htm`,
+ item: items,
+ };
+}
diff --git a/lib/routes/hrbeu/job/calendar.ts b/lib/routes/hrbeu/job/calendar.ts
index 783bb24e417a5d..fcc61d51363d3c 100644
--- a/lib/routes/hrbeu/job/calendar.ts
+++ b/lib/routes/hrbeu/job/calendar.ts
@@ -1,5 +1,5 @@
import { Route } from '@/types';
-import got from '@/utils/got';
+import ofetch from '@/utils/ofetch';
import { load } from 'cheerio';
const rootUrl = 'http://job.hrbeu.edu.cn';
@@ -27,8 +27,8 @@ export const route: Route = {
handler,
url: 'job.hrbeu.edu.cn/*',
description: `| 通知公告 | 热点新闻 |
- | :------: | :------: |
- | tzgg | rdxw |
+| :------: | :------: |
+| tzgg | rdxw |
#### 大型招聘会 {#ha-er-bin-gong-cheng-da-xue-jiu-ye-fu-wu-ping-tai-da-xing-zhao-pin-hui}
@@ -44,11 +44,11 @@ async function handler() {
month < 10 ? (strmMonth = '0' + month) : (strmMonth = month);
const day = date.getDate();
- const response = await got('http://job.hrbeu.edu.cn/HrbeuJY/Web/Employ/QueryCalendar', {
- searchParams: {
+ const response = await ofetch('http://job.hrbeu.edu.cn/HrbeuJY/Web/Employ/QueryCalendar', {
+ query: {
yearMonth: year + '-' + strmMonth,
},
- }).json();
+ });
let link = '';
for (let i = 0, l = response.length; i < l; i++) {
@@ -58,9 +58,11 @@ async function handler() {
}
}
- const todayResponse = await got(`${rootUrl}${link}`);
+ const todayResponse = await ofetch(`${rootUrl}${link}`, {
+ parseResponse: (txt) => txt,
+ });
- const $ = load(todayResponse.data);
+ const $ = load(todayResponse);
const list = $('li.clearfix')
.map((_, item) => ({
diff --git a/lib/routes/hrbeu/job/list.ts b/lib/routes/hrbeu/job/list.ts
index 2134f6eb038e30..e21e8c185d922c 100644
--- a/lib/routes/hrbeu/job/list.ts
+++ b/lib/routes/hrbeu/job/list.ts
@@ -33,8 +33,8 @@ export const route: Route = {
name: '就业服务平台',
maintainers: ['Derekmini'],
description: `| 通知公告 | 热点新闻 |
- | :------: | :------: |
- | tzgg | rdxw |`,
+| :------: | :------: |
+| tzgg | rdxw |`,
handler,
};
diff --git a/lib/routes/hrbeu/namespace.ts b/lib/routes/hrbeu/namespace.ts
index c6ca2928b3d3ff..d920202e33b0bd 100644
--- a/lib/routes/hrbeu/namespace.ts
+++ b/lib/routes/hrbeu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '哈尔滨工程大学',
url: 'yjsy.hrbeu.edu.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hrbeu/sec/list.ts b/lib/routes/hrbeu/sec/list.ts
new file mode 100644
index 00000000000000..795f861908aa28
--- /dev/null
+++ b/lib/routes/hrbeu/sec/list.ts
@@ -0,0 +1,80 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+const rootUrl = 'http://sec.hrbeu.edu.cn';
+
+export const route: Route = {
+ path: '/sec/:id',
+ categories: ['university'],
+ example: '/hrbeu/sec/xshd',
+ parameters: { id: '栏目编号,由 `URL` 中获取。' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['sec.hrbeu.edu.cn/:id/list.htm'],
+ },
+ ],
+ name: '船舶工程学院',
+ maintainers: ['Chi-hong22'],
+ handler,
+ description: `| 学院要闻 | 学术活动 | 通知公告 | 学科方向 |
+| :------: | :------: |:------: | :------: |
+| xyyw | xshd | 229 | xkfx |`,
+};
+
+async function handler(ctx) {
+ const id = ctx.req.param('id');
+ const response = await got(`${rootUrl}/${id}/list.htm`, {
+ headers: {
+ Referer: rootUrl,
+ },
+ });
+
+ const $ = load(response.data);
+
+ const bigTitle = $('div [class=lanmuInnerMiddleBigClass_right]').find('div [portletmode=simpleColumnAttri]').text().replaceAll(/[\s·]/g, '').trim();
+
+ const list = $('li.list_item')
+ .toArray()
+ .map((item) => {
+ let link = $(item).find('a').attr('href');
+ if (link && link.includes('page.htm')) {
+ link = `${rootUrl}${link}`;
+ }
+ return {
+ title: $(item).find('a').attr('title'),
+ pubDate: parseDate($(item).find('span.Article_PublishDate').text()),
+ link,
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ if (item.link.includes('page.htm')) {
+ const detailResponse = await got(item.link);
+ const content = load(detailResponse.data);
+ item.description = content('div.wp_articlecontent').html();
+ } else {
+ item.description = '本文需跳转,请点击标题后阅读';
+ }
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: '船舶工程学院 - ' + bigTitle,
+ link: rootUrl.concat('/', id, '/list.htm'),
+ item: items,
+ };
+}
diff --git a/lib/routes/hrbeu/uae/news.ts b/lib/routes/hrbeu/uae/news.ts
index 2d24e65f2ec4de..413f574fca4d9e 100644
--- a/lib/routes/hrbeu/uae/news.ts
+++ b/lib/routes/hrbeu/uae/news.ts
@@ -27,8 +27,8 @@ export const route: Route = {
maintainers: [],
handler,
description: `| 新闻动态 | 通知公告 | 科学研究 / 科研动态 |
- | :------: | :------: | :-----------------: |
- | xwdt | tzgg | kxyj-kydt |`,
+| :------: | :------: | :-----------------: |
+| xwdt | tzgg | kxyj-kydt |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hrbeu/ugs/news.ts b/lib/routes/hrbeu/ugs/news.ts
index eb605ae20352b5..0eeca3dcb0e4d1 100644
--- a/lib/routes/hrbeu/ugs/news.ts
+++ b/lib/routes/hrbeu/ugs/news.ts
@@ -78,9 +78,9 @@ export const route: Route = {
handler,
description: `author 列表:
- | 教务处 | 实践教学与交流处 | 教育评估处 | 专业建设处 | 国家大学生文化素质基地 | 教师教学发展中心 | 综合办公室 | 工作通知 |
- | ------ | ---------------- | ---------- | ---------- | ---------------------- | ---------------- | ---------- | -------- |
- | jwc | sjjxyjlzx | jypgc | zyjsc | gjdxswhszjd | jsjxfzzx | zhbgs | gztz |
+| 教务处 | 实践教学与交流处 | 教育评估处 | 专业建设处 | 国家大学生文化素质基地 | 教师教学发展中心 | 综合办公室 | 工作通知 |
+| ------ | ---------------- | ---------- | ---------- | ---------------------- | ---------------- | ---------- | -------- |
+| jwc | sjjxyjlzx | jypgc | zyjsc | gjdxswhszjd | jsjxfzzx | zhbgs | gztz |
category 列表:
@@ -88,41 +88,41 @@ export const route: Route = {
教务处:
- | 教学安排 | 考试管理 | 学籍管理 | 外语统考 | 成绩管理 |
- | -------- | -------- | -------- | -------- | -------- |
- | jxap | ksgl | xjgl | wytk | cjgl |
+| 教学安排 | 考试管理 | 学籍管理 | 外语统考 | 成绩管理 |
+| -------- | -------- | -------- | -------- | -------- |
+| jxap | ksgl | xjgl | wytk | cjgl |
实践教学与交流处:
- | 实验教学 | 实验室建设 | 校外实习 | 学位论文 | 课程设计 | 创新创业 | 校际交流 |
- | -------- | ---------- | -------- | -------- | -------- | -------- | -------- |
- | syjx | sysjs | xwsx | xwlw | kcsj | cxcy | xjjl |
+| 实验教学 | 实验室建设 | 校外实习 | 学位论文 | 课程设计 | 创新创业 | 校际交流 |
+| -------- | ---------- | -------- | -------- | -------- | -------- | -------- |
+| syjx | sysjs | xwsx | xwlw | kcsj | cxcy | xjjl |
教育评估处:
- | 教学研究与教学成果 | 质量监控 |
- | ------------------ | -------- |
- | jxyjyjxcg | zljk |
+| 教学研究与教学成果 | 质量监控 |
+| ------------------ | -------- |
+| jxyjyjxcg | zljk |
专业建设处:
- | 专业与教材建设 | 陈赓实验班 | 教学名师与优秀主讲教师 | 课程建设 | 双语教学 |
- | -------------- | ---------- | ---------------------- | -------- | -------- |
- | zyyjcjs | cgsyb | jxmsyyxzjjs | kcjs | syjx |
+| 专业与教材建设 | 陈赓实验班 | 教学名师与优秀主讲教师 | 课程建设 | 双语教学 |
+| -------------- | ---------- | ---------------------- | -------- | -------- |
+| zyyjcjs | cgsyb | jxmsyyxzjjs | kcjs | syjx |
国家大学生文化素质基地:无
教师教学发展中心:
- | 教师培训 |
- | -------- |
- | jspx |
+| 教师培训 |
+| -------- |
+| jspx |
综合办公室:
- | 联系课程 |
- | -------- |
- | lxkc |
+| 联系课程 |
+| -------- |
+| lxkc |
工作通知:无`,
};
diff --git a/lib/routes/hrbeu/yjsy/list.ts b/lib/routes/hrbeu/yjsy/list.ts
index 041103126332f5..c25c806359a443 100644
--- a/lib/routes/hrbeu/yjsy/list.ts
+++ b/lib/routes/hrbeu/yjsy/list.ts
@@ -27,8 +27,8 @@ export const route: Route = {
maintainers: ['Derekmini'],
handler,
description: `| 通知公告 | 新闻动态 | 学籍注册 | 奖助学金 | 其他 |
- | :------: | :------: | :------: | :------: | :--: |
- | 2981 | 2980 | 3009 | 3011 | ... |`,
+| :------: | :------: | :------: | :------: | :--: |
+| 2981 | 2980 | 3009 | 3011 | ... |`,
};
async function handler(ctx) {
diff --git a/lib/routes/hrbust/cs.ts b/lib/routes/hrbust/cs.ts
new file mode 100644
index 00000000000000..940a232fdbd6f3
--- /dev/null
+++ b/lib/routes/hrbust/cs.ts
@@ -0,0 +1,100 @@
+import { Route, ViewType } from '@/types';
+import cache from '@/utils/cache';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+import { load } from 'cheerio';
+import { ofetch } from 'ofetch';
+
+export const route: Route = {
+ path: '/cs/:category?',
+ name: '计算机学院',
+ url: 'cs.hrbust.edu.cn',
+ maintainers: ['cscnk52'],
+ handler,
+ example: '/hrbust/cs',
+ parameters: { category: '栏目标识,默认为 3709(学院要闻)' },
+ description: `| 通知公告 | 学院要闻 | 常用下载 | 博士后流动站 | 学生指导 | 科研动态 | 科技成果 | 党建理论 | 党建学习 | 党建活动 | 党建风采 | 团学组织 | 学生党建 | 学生活动 | 心理健康 | 青春榜样 | 就业工作 | 校友风采 | 校庆专栏 | 专业介绍 | 本科生培养方案 | 硕士生培养方案 | 能力作风建设 | 博士生培养方案 | 省级实验教学示范中心 | 喜迎二十大系列活动 | 学习贯彻省十三次党代会精神 |
+|----------|----------|----------|--------------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------------|----------------|--------------|----------------|----------------------|--------------------|----------------------------|
+| 3708 | 3709 | 3710 | 3725 | 3729 | 3732 | 3733 | 3740 | 3741 | 3742 | 3743 | 3744 | 3745 | 3746 | 3747 | 3748 | 3751 | 3752 | 3753 | 3755 | 3756 | 3759 | nlzfjs | pyfa | sjsyjxsfzx | srxxgcddesdjs | xxgcssscddhjs |`,
+ categories: ['university'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ supportRadar: true,
+ },
+ radar: [
+ {
+ source: ['cs.hrbust.edu.cn/:category/list.htm'],
+ target: '/cs/:category',
+ },
+ {
+ source: ['cs.hrbust.edu.cn'],
+ target: '/cs',
+ },
+ ],
+ view: ViewType.Notifications,
+};
+
+async function handler(ctx) {
+ const rootUrl = 'https://cs.hrbust.edu.cn/';
+ const { category = 3709 } = ctx.req.param();
+ const columnUrl = `${rootUrl}${category}/list.htm`;
+ const response = await ofetch(columnUrl);
+ const $ = load(response);
+ const bigTitle = $('li.col_title').text();
+
+ const list = $('div.col_news_con li.news')
+ .toArray()
+ .map((item) => {
+ const element = $(item);
+ const link = new URL(element.find('a').attr('href'), rootUrl).href;
+ const pubDateText = element.find('span.news_meta').text().trim();
+ const pubDate = pubDateText ? timezone(parseDate(pubDateText), +8) : null;
+ return {
+ title: element.find('a').text().trim(),
+ pubDate,
+ link,
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ if (!item.link.startsWith(rootUrl)) {
+ item.description = '本文需跳转,请点击原文链接后阅读';
+ return item;
+ }
+
+ const response = await ofetch(item.link);
+ const $ = load(response);
+ const content = $('div.wp_articlecontent');
+
+ content.find('[style]').removeAttr('style');
+ content.find('font').contents().unwrap();
+ content.html(content.html()?.replaceAll(' ', ''));
+ content.find('[align]').removeAttr('align');
+
+ const author = $('span.arti_publisher').text().replace('发布者:', '').trim();
+
+ return {
+ title: item.title,
+ link: item.link,
+ pubDate: item.pubDate,
+ description: content.html(),
+ author,
+ };
+ })
+ )
+ );
+
+ return {
+ title: `${bigTitle} - 哈尔滨理工大学计算机学院`,
+ link: columnUrl,
+ language: 'zh-CN',
+ item: items,
+ };
+}
diff --git a/lib/routes/hrbust/gzc.ts b/lib/routes/hrbust/gzc.ts
new file mode 100644
index 00000000000000..f76ef3c56caacc
--- /dev/null
+++ b/lib/routes/hrbust/gzc.ts
@@ -0,0 +1,97 @@
+import { Route, ViewType } from '@/types';
+import cache from '@/utils/cache';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+import { load } from 'cheerio';
+import { ofetch } from 'ofetch';
+
+export const route: Route = {
+ path: '/gzc/:category?',
+ name: '国有资产管理处',
+ url: 'gzc.hrbust.edu.cn',
+ maintainers: ['cscnk52'],
+ handler,
+ example: '/hrbust/gzc',
+ parameters: { category: '栏目标识,默认为 1305(热点新闻)' },
+ description: `| 政策规章 | 资料下载 | 处务公开 | 招标信息 | 岗位职责 | 管理办法 | 物资处理 | 工作动态 | 热点新闻 |
+|----------|----------|----------|----------|----------|----------|----------|----------|----------|
+| 1287 | 1288 | 1289 | 1291 | 1300 | 1301 | 1302 | 1304 | 1305 |`,
+ categories: ['university'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ supportRadar: true,
+ },
+ radar: [
+ {
+ source: ['gzc.hrbust.edu.cn/:category/list.htm'],
+ target: '/gzc/:category',
+ },
+ {
+ source: ['gzc.hrbust.edu.cn'],
+ target: '/gzc',
+ },
+ ],
+ view: ViewType.Notifications,
+};
+
+async function handler(ctx) {
+ const rootUrl = 'https://gzc.hrbust.edu.cn/';
+ const { category = 1305 } = ctx.req.param();
+ const columnUrl = `${rootUrl}${category}/list.htm`;
+ const response = await ofetch(columnUrl);
+ const $ = load(response);
+ const bigTitle = $('li.col-title').text();
+
+ const list = $('ul.wp_article_list li.list_item')
+ .toArray()
+ .map((item) => {
+ const element = $(item);
+ const link = new URL(element.find('a').attr('href'), rootUrl).href;
+ const pubDateText = element.find('span.Article_PublishDate').text().trim();
+ const pubDate = pubDateText ? timezone(parseDate(pubDateText), +8) : null;
+ return {
+ title: element.find('a').text().trim(),
+ pubDate,
+ link,
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ if (!item.link.startsWith(rootUrl)) {
+ item.description = '本文需跳转,请点击原文链接后阅读';
+ return item;
+ }
+
+ const response = await ofetch(item.link);
+ const $ = load(response);
+ const content = $('div.wp_articlecontent');
+
+ content.find('[style]').removeAttr('style');
+ content.find('font').contents().unwrap();
+ content.html(content.html()?.replaceAll(' ', ''));
+ content.find('[align]').removeAttr('align');
+
+ return {
+ title: item.title,
+ link: item.link,
+ pubDate: item.pubDate,
+ description: content.html(),
+ };
+ })
+ )
+ );
+
+ return {
+ title: `${bigTitle} - 哈尔滨理工大学国有资产管理处`,
+ link: columnUrl,
+ language: 'zh-CN',
+ item: items,
+ };
+}
diff --git a/lib/routes/hrbust/jwzx.ts b/lib/routes/hrbust/jwzx.ts
index 15c06b29ca1aa2..37f8a57fa266b0 100644
--- a/lib/routes/hrbust/jwzx.ts
+++ b/lib/routes/hrbust/jwzx.ts
@@ -1,28 +1,26 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import cache from '@/utils/cache';
-import got from '@/utils/got';
-import utils from './utils';
-
-const typeMap = {
- 351: {
- name: '名师风采',
- },
- 353: {
- name: '热点新闻',
- },
- 354: {
- name: '教务公告',
- },
- 355: {
- name: '教学新闻',
- },
-};
+import ofetch from '@/utils/ofetch';
+import timezone from '@/utils/timezone';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/jwzx/:type?/:page?',
- categories: ['university'],
+ name: '教务处',
+ url: 'jwzx.hrbust.edu.cn',
+ maintainers: ['LenaNouzen', 'cscnk52'],
+ handler,
example: '/hrbust/jwzx',
- parameters: { type: '分类名,默认为教务公告', page: '文章数,默认为12' },
+ parameters: { type: '分类编号,默认为 354(教务公告),具体见下表', page: '文章数,默认为 12' },
+ description: `::: tip
+- type 可以从 URL 中的 columnId 获取。
+- 由于源站未提供精确时间,只能抓取日期粒度的时间。
+:::
+| 组织机构 | 工作职责 | 专业设置 | 教务信箱 | 名师风采 | 热点新闻 | 教务公告 | 教学新闻 | 教学管理 | 教务管理 | 学籍管理 | 实践教学 | 系统使用动画 | 教学管理 | 教务管理 | 学籍管理 | 实验教学 | 实践教学 | 教研论文教材认定 | 教学管理 | 学籍管理 | 实践教学 | 网络教学 | 多媒体教室管理 | 实验教学与实验室管理 | 教学成果 | 国创计划 | 学科竞赛 | 微专业 | 众创空间 | 示范基地 | 学生社团 |
+|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|--------------|----------|----------|----------|----------|----------|------------------|----------|----------|----------|----------|----------------|----------------------|----------|----------|----------|--------|----------|----------|----------|
+| 339 | 340 | 342 | 346 | 351 | 353 | 354 | 355 | 442 | 443 | 444 | 445 | 2106 | 2332 | 2333 | 2334 | 2335 | 2336 | 2730 | 2855 | 2857 | 2859 | 3271 | 3508 | 3519 | 3981 | 4057 | 4058 | 4059 | 4060 | 4061 | 4062 |`,
+ categories: ['university'],
features: {
requireConfig: false,
requirePuppeteer: false,
@@ -30,30 +28,69 @@ export const route: Route = {
supportBT: false,
supportPodcast: false,
supportScihub: false,
+ supportRadar: true,
},
- name: '教务处',
- maintainers: ['LenaNouzen'],
- handler,
- description: `| 名师风采 | 热点新闻 | 教务公告 | 教学新闻 |
- | -------- | -------- | -------- | -------- |
- | 351 | 353 | 354 | 355 |`,
+ radar: [
+ {
+ source: ['jwzx.hrbust.edu.cn/homepage/index.do'],
+ target: '/jwzx',
+ },
+ ],
+ view: ViewType.Notifications,
};
async function handler(ctx) {
- const page = ctx.req.param('page') || '12';
- const base = utils.columnIdBase(ctx.req.param('type')) + '&pagingNumberPer=' + page;
- const res = await got(base);
- const info = utils.fetchAllArticle(res.data, utils.JWZXBASE);
+ const rootUrl = 'http://jwzx.hrbust.edu.cn/homepage/';
+ const { type = 354, page = 12 } = ctx.req.param();
+ const columnUrl = rootUrl + 'infoArticleList.do?columnId=' + type + '&pagingNumberPer=' + page;
+ const response = await ofetch(columnUrl);
+ const $ = load(response);
+
+ const bigTitle = $('.columnTitle .wow span').text().trim();
+
+ const list = $('div.articleList li')
+ .toArray()
+ .map((item) => {
+ const element = $(item);
+ const link = new URL(element.find('a').attr('href'), rootUrl).href;
+ const title = element.find('a').text().trim();
+ const pubDateText = element.find('span').text().trim();
+ const pubDate = timezone(parseDate(pubDateText), +8);
+ return {
+ title,
+ link,
+ pubDate,
+ };
+ });
- const details = await Promise.all(info.map((e) => utils.detailPage(e, cache)));
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ if (!item.link.startsWith(rootUrl)) {
+ item.description = '本文需跳转,请点击原文链接后阅读';
+ return item;
+ }
- // ctx.set('json', {
- // info,
- // };
+ const response = await ofetch(item.link);
+ const $ = load(response);
+ const body = $('div.body');
+ body.find('[style]').removeAttr('style');
+ body.find('font').contents().unwrap();
+ body.html(body.html()?.replaceAll(' ', ''));
+ body.find('[align]').removeAttr('align');
+ item.description = body.html();
+ if (item.description === null) {
+ item.description = '解析正文失败';
+ }
+ return item;
+ })
+ )
+ );
return {
- title: '哈理工教务处' + typeMap[ctx.req.param('type') || 354].name,
- link: base,
- item: details,
+ title: `${bigTitle} - 哈尔滨理工大学教务处`,
+ link: columnUrl,
+ language: 'zh-CN',
+ item: items,
};
}
diff --git a/lib/routes/hrbust/lib.ts b/lib/routes/hrbust/lib.ts
new file mode 100644
index 00000000000000..6a9aee91b8650f
--- /dev/null
+++ b/lib/routes/hrbust/lib.ts
@@ -0,0 +1,97 @@
+import { Route, ViewType } from '@/types';
+import cache from '@/utils/cache';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+import { load } from 'cheerio';
+import { ofetch } from 'ofetch';
+
+export const route: Route = {
+ path: '/lib/:category?',
+ name: '图书馆',
+ url: 'lib.hrbust.edu.cn',
+ maintainers: ['cscnk52'],
+ handler,
+ example: '/hrbust/lib',
+ parameters: { category: '栏目标识,默认为 3421(公告消息)' },
+ description: `| 公告消息 | 资源动态 | 参考中心 | 常用工具 | 外借服务 | 报告厅及研讨间服务 | 外文引进数据库 | 外文电子图书 | 外文试用数据库 | 中文引进数据库 | 中文电子图书 | 中文试用数据库 |
+|----------|----------|----------|----------|----------|--------------------|----------------|--------------|----------------|----------------|--------------|----------------|
+| 3421 | 3422 | ckzx | cygj | wjfw | ytjfw | yw | yw_3392 | yw_3395 | zw | zw_3391 | zw_3394 |`,
+ categories: ['university'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ supportRadar: true,
+ },
+ radar: [
+ {
+ source: ['lib.hrbust.edu.cn/:category/list.htm'],
+ target: '/lib/:category',
+ },
+ {
+ source: ['lib.hrbust.edu.cn'],
+ target: '/lib',
+ },
+ ],
+ view: ViewType.Notifications,
+};
+
+async function handler(ctx) {
+ const rootUrl = 'https://lib.hrbust.edu.cn/';
+ const { category = 3421 } = ctx.req.param();
+ const columnUrl = `${rootUrl}${category}/list.htm`;
+ const response = await ofetch(columnUrl);
+ const $ = load(response);
+ const bigTitle = $('span.Column_Anchor').text();
+
+ const list = $('ul.tu_b3 li:not([class])')
+ .toArray()
+ .map((item) => {
+ const element = $(item);
+ const link = new URL(element.find('a').attr('href'), rootUrl).href;
+ const pubDateText = element.find('span').text().trim();
+ const pubDate = pubDateText ? timezone(parseDate(pubDateText), +8) : null;
+ return {
+ title: element.find('a').text().trim(),
+ pubDate,
+ link,
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ if (!item.link.startsWith(rootUrl)) {
+ item.description = '本文需跳转,请点击原文链接后阅读';
+ return item;
+ }
+
+ const response = await ofetch(item.link);
+ const $ = load(response);
+ const content = $('div.wp_articlecontent');
+
+ content.find('[style]').removeAttr('style');
+ content.find('font').contents().unwrap();
+ content.html(content.html()?.replaceAll(' ', ''));
+ content.find('[align]').removeAttr('align');
+
+ return {
+ title: item.title,
+ link: item.link,
+ pubDate: item.pubDate,
+ description: content.html(),
+ };
+ })
+ )
+ );
+
+ return {
+ title: `${bigTitle} - 哈尔滨理工大学图书馆`,
+ link: columnUrl,
+ language: 'zh-CN',
+ item: items,
+ };
+}
diff --git a/lib/routes/hrbust/namespace.ts b/lib/routes/hrbust/namespace.ts
index f3c31621f4cfa1..a02e2c687328d4 100644
--- a/lib/routes/hrbust/namespace.ts
+++ b/lib/routes/hrbust/namespace.ts
@@ -2,5 +2,7 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '哈尔滨理工大学',
- url: 'jwzx.hrbust.edu.cn',
+ url: 'hrbust.edu.cn',
+ categories: ['university'],
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hrbust/news.ts b/lib/routes/hrbust/news.ts
new file mode 100644
index 00000000000000..8cc2d00ee63be4
--- /dev/null
+++ b/lib/routes/hrbust/news.ts
@@ -0,0 +1,104 @@
+import { Route, ViewType } from '@/types';
+import cache from '@/utils/cache';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+import ofetch from '@/utils/ofetch';
+
+export const route: Route = {
+ path: '/news/:category?',
+ name: '新闻网',
+ url: 'news.hrbust.edu.cn',
+ maintainers: ['cscnk52'],
+ handler,
+ example: '/hrbust/news',
+ parameters: { category: '栏目标识,默认为 lgyw(理工要闻)' },
+ description: `| 理工要闻 | 新闻导读 | 图文报道 | 综合新闻 | 教学科研 | 院处动态 | 学术科创 | 交流合作 | 学生天地 | 招生就业 | 党建思政 | 在线播放 | 理工人物 | 理工校报 | 媒体理工 | 讲座论坛 | 人才招聘 | 学科建设 |
+|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|----------|
+| lgyw | xwdd | twbd | zhenew | jxky | ycdt | xskc | jlhz | xstd | zsjy | djsz | zxbf | lgrw | lgxb | mtlg | jzlt | rczp | xkjs |`,
+ categories: ['university'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ supportRadar: true,
+ },
+ radar: [
+ {
+ source: ['news.hrbust.edu.cn/:category.htm'],
+ target: '/news/:category',
+ },
+ {
+ source: ['news.hrbust.edu.cn/'],
+ target: '/news/',
+ },
+ ],
+ view: ViewType.Notifications,
+};
+
+async function handler(ctx) {
+ const rootUrl = 'https://news.hrbust.edu.cn/';
+ const { category = 'lgyw' } = ctx.req.param();
+ const columnUrl = `${rootUrl}${category}.htm`;
+ const response = await ofetch(columnUrl);
+ const $ = load(response);
+
+ const bigTitle = $('title').text().split('-')[0].trim();
+
+ const list = $('li[id^=line_u10]')
+ .toArray()
+ .map((item) => {
+ const element = $(item);
+ const link = new URL(element.find('a').attr('href'), rootUrl).href;
+ const pubDateText = element.find('span').text().trim();
+ const pubDate = pubDateText ? timezone(parseDate(pubDateText), +8) : null;
+ return {
+ title: element.find('a').text().trim(),
+ pubDate,
+ link,
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ if (!item.link.startsWith(rootUrl)) {
+ item.description = '本文需跳转,请点击原文链接后阅读';
+ return item;
+ }
+
+ const detailResponse = await ofetch(item.link);
+ const content = load(detailResponse);
+
+ const dateText = content('p.xinxi span:contains("日期时间:")').text().replace('日期时间:', '').trim();
+ const pubTime = dateText ? timezone(parseDate(dateText), +8) : null;
+ if (pubTime) {
+ item.pubDate = pubTime;
+ }
+
+ const author = content('p.xinxi span:contains("作者:")').text().replace('作者:', '').trim();
+ item.author = author || null;
+
+ const newsContent = content('div.v_news_content') || '解析正文失败';
+ const listAttachments = content('ul[style="list-style-type:none;"] a');
+ let listAttachmentsHtml = '';
+ listAttachments.each((_, a_element) => {
+ listAttachmentsHtml += '
' + content(a_element).prop('outerHTML');
+ });
+
+ item.description = newsContent + listAttachmentsHtml;
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: `${bigTitle} - 哈尔滨理工大学新闻网`,
+ link: columnUrl,
+ language: 'zh-CN',
+ item: items,
+ };
+}
diff --git a/lib/routes/hrbust/nic.ts b/lib/routes/hrbust/nic.ts
new file mode 100644
index 00000000000000..f6d0e3bc50aaad
--- /dev/null
+++ b/lib/routes/hrbust/nic.ts
@@ -0,0 +1,97 @@
+import { Route, ViewType } from '@/types';
+import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+import { load } from 'cheerio';
+
+export const route: Route = {
+ path: '/nic/:category?',
+ name: '网络信息中心',
+ url: 'nic.hrbust.edu.cn',
+ maintainers: ['cscnk52'],
+ handler,
+ example: '/hrbust/nic',
+ parameters: { category: '栏目标识,默认为 3988(新闻动态)' },
+ description: `| 服务指南 | 常见问题 | 新闻动态 | 通知公告 | 国家政策法规 | 学校规章制度 | 部门规章制度 | 宣传教育 | 安全法规 |
+|----------|----------|----------|----------|--------------|--------------|--------------|----------|----------|
+| 3982 | 3983 | 3988 | 3989 | 3990 | 3991 | 3992 | 3993 | 3994 |`,
+ categories: ['university'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ supportRadar: true,
+ },
+ radar: [
+ {
+ source: ['nic.hrbust.edu.cn/:category/list.htm'],
+ target: '/nic/:category',
+ },
+ {
+ source: ['nic.hrbust.edu.cn/'],
+ target: '/nic/',
+ },
+ ],
+ view: ViewType.Notifications,
+};
+
+async function handler(ctx) {
+ const rootUrl = 'https://nic.hrbust.edu.cn/';
+ const { category = 3988 } = ctx.req.param();
+ const columnUrl = `${rootUrl}${category}/list.htm`;
+ const response = await ofetch(columnUrl);
+ const $ = load(response);
+
+ const bigTitle = $('li.col_title').text();
+
+ const list = $('ul.news_list.list2 li')
+ .toArray()
+ .map((item) => {
+ const element = $(item);
+ const title = element.find('a').text().trim();
+ const link = new URL(element.find('a').attr('href'), rootUrl).href;
+
+ const pubDateText = element.find('span.news_meta').text().trim();
+ const pubDate = pubDateText ? timezone(parseDate(pubDateText), +8) : null;
+ return {
+ title,
+ pubDate,
+ link,
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ if (!item.link.startsWith(rootUrl)) {
+ item.description = '本文需跳转,请点击原文链接后阅读';
+ return item;
+ }
+
+ const response = await ofetch(item.link);
+ const $ = load(response);
+
+ item.author = $('span.arti_publisher').text().replace('发布者:', '').trim();
+
+ const body = $('div.wp_articlecontent');
+ body.find('[style]').removeAttr('style');
+ body.find('font').contents().unwrap();
+ body.html(body.html()?.replaceAll(' ', ''));
+ body.find('[align]').removeAttr('align');
+ item.description = body.html();
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: `${bigTitle} - 哈尔滨理工大学网络信息中心`,
+ link: columnUrl,
+ language: 'zh-CN',
+ item: items,
+ };
+}
diff --git a/lib/routes/hrbust/templates/description.art b/lib/routes/hrbust/templates/description.art
deleted file mode 100644
index 7755ee9f6b0032..00000000000000
--- a/lib/routes/hrbust/templates/description.art
+++ /dev/null
@@ -1 +0,0 @@
-{{@ desc }}
diff --git a/lib/routes/hrbust/utils.ts b/lib/routes/hrbust/utils.ts
deleted file mode 100644
index f78ad868fbfdc4..00000000000000
--- a/lib/routes/hrbust/utils.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { getCurrentPath } from '@/utils/helpers';
-const __dirname = getCurrentPath(import.meta.url);
-
-import { load } from 'cheerio';
-import got from '@/utils/got';
-import { art } from '@/utils/render';
-import path from 'node:path';
-import { parseDate } from '@/utils/parse-date';
-import timezone from '@/utils/timezone';
-
-// const base = 'http://hrbust.edu.cn';
-const jwzxBase = 'http://jwzx.hrbust.edu.cn/homepage/';
-
-const columnIdBase = (id) => (id ? `${jwzxBase}infoArticleList.do?columnId=${id}` : `${jwzxBase}infoArticleList.do?columnId=354`);
-
-const renderDesc = (desc) =>
- art(path.join(__dirname, 'templates/description.art'), {
- desc,
- });
-
-const detailPage = (e, cache) =>
- cache.tryGet(e.detailPage, async () => {
- const result = await got(e.detailPage);
- const $ = load(result.data);
- const title = $('div#article h2').text().trim();
- const desc =
- $('div.body').html() &&
- $('div.body')
- .html()
- .replaceAll(/style="(.*?)"/g, '')
- .trim();
- const pubDate = timezone(parseDate($('div#articleInfo ul li').first().text().replaceAll('发布日期:', '').trim()), +8);
- return {
- title: e.title || title,
- description: renderDesc(desc),
- link: e.detailPage,
- pubDate: e.pubDate || pubDate,
- };
- });
-
-const fetchAllArticle = (data, base) => {
- const $ = load(data);
- const article = $('.articleList li div');
- const info = article
- .map((i, e) => {
- const c = load(e);
- const r = {
- title: c('a').first().text().trim(),
- detailPage: new URL(c('a').attr('href'), base).href,
- pubDate: timezone(parseDate(c('span').first().text().trim()), +8),
- };
- return r;
- })
- .get();
- return info;
-};
-
-export default {
- // BASE: base,
- JWZXBASE: jwzxBase,
- columnIdBase,
- fetchAllArticle,
- detailPage,
- renderDesc,
-};
diff --git a/lib/routes/huanqiu/index.ts b/lib/routes/huanqiu/index.ts
index 2b70b76f7f436b..3f4d03a13d3fcd 100644
--- a/lib/routes/huanqiu/index.ts
+++ b/lib/routes/huanqiu/index.ts
@@ -40,8 +40,8 @@ export const route: Route = {
handler,
url: 'huanqiu.com/',
description: `| 国内新闻 | 国际新闻 | 军事 | 台海 | 评论 |
- | -------- | -------- | ---- | ------ | ------- |
- | china | world | mil | taiwai | opinion |`,
+| -------- | -------- | ---- | ------ | ------- |
+| china | world | mil | taiwai | opinion |`,
};
async function handler(ctx) {
diff --git a/lib/routes/huanqiu/namespace.ts b/lib/routes/huanqiu/namespace.ts
index 22d2e16b7d38db..6e5465fabeed26 100644
--- a/lib/routes/huanqiu/namespace.ts
+++ b/lib/routes/huanqiu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '环球网',
url: 'huanqiu.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hubu/index.ts b/lib/routes/hubu/index.ts
index 3b87dfd621cddd..c8f6998a675ea9 100644
--- a/lib/routes/hubu/index.ts
+++ b/lib/routes/hubu/index.ts
@@ -90,13 +90,13 @@ export const route: Route = {
handler,
example: '/hubu/www/index/tzgg',
parameters: { category: '分类,可在对应分类页 URL 中找到,默认为[通知公告](https://www.hubu.edu.cn/index/tzgg.htm)' },
- description: `:::tip
+ description: `::: tip
若订阅 [通知公告](https://www.hubu.edu.cn/index/tzgg.htm),网址为 \`https://www.hubu.edu.cn/index/tzgg.htm\`。截取 \`https://www.hubu.edu.cn/\` 到末尾 \`.htm\` 的部分 \`index/tzgg\` 作为参数填入,此时路由为 [\`/hubu/www/index/tzgg\`](https://rsshub.app/hubu/www/index/tzgg)。
- :::
+:::
- | 通知公告 | 学术预告 |
- | ---------- | ---------- |
- | index/tzgg | index/xsyg |
+| 通知公告 | 学术预告 |
+| ---------- | ---------- |
+| index/tzgg | index/xsyg |
`,
categories: ['university'],
diff --git a/lib/routes/hubu/namespace.ts b/lib/routes/hubu/namespace.ts
index 185d6e7298229d..a900820eef2278 100644
--- a/lib/routes/hubu/namespace.ts
+++ b/lib/routes/hubu/namespace.ts
@@ -5,4 +5,5 @@ export const namespace: Namespace = {
url: 'hubu.edu.cn',
categories: ['university'],
description: '',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/hubu/zhxy.ts b/lib/routes/hubu/zhxy.ts
index 3f8e61a76417e8..b5e9801a34c58b 100644
--- a/lib/routes/hubu/zhxy.ts
+++ b/lib/routes/hubu/zhxy.ts
@@ -90,37 +90,37 @@ export const route: Route = {
handler,
example: '/hubu/zhxy/index/tzgg',
parameters: { category: '分类,可在对应分类页 URL 中找到,默认为[通知公告](https://zhxy.hubu.edu.cn/index/tzgg.htm)' },
- description: `:::tip
+ description: `::: tip
若订阅 [通知公告](https://zhxy.hubu.edu.cn/index/tzgg.htm),网址为 \`https://zhxy.hubu.edu.cn/index/tzgg.htm\`。截取 \`https://zhxy.hubu.edu.cn/\` 到末尾 \`.htm\` 的部分 \`index/tzgg\` 作为参数填入,此时路由为 [\`/hubu/zhxy/index/tzgg\`](https://rsshub.app/hubu/zhxy/index/tzgg)。
- :::
+:::
- | [通知公告](https://zhxy.hubu.edu.cn/index/tzgg.htm) | [新闻动态](https://zhxy.hubu.edu.cn/index/xwdt.htm) |
- | --------------------------------------------------- | --------------------------------------------------- |
- | index/tzgg | index/xwdt |
+| [通知公告](https://zhxy.hubu.edu.cn/index/tzgg.htm) | [新闻动态](https://zhxy.hubu.edu.cn/index/xwdt.htm) |
+| --------------------------------------------------- | --------------------------------------------------- |
+| index/tzgg | index/xwdt |
- #### [人才培养](https://zhxy.hubu.edu.cn/rcpy.htm)
+#### [人才培养](https://zhxy.hubu.edu.cn/rcpy.htm)
- | [人才培养](https://zhxy.hubu.edu.cn/rcpy.htm) | [本科生教育](https://zhxy.hubu.edu.cn/rcpy/bksjy.htm) | [研究生教育](https://zhxy.hubu.edu.cn/rcpy/yjsjy.htm) | [招生与就业](https://zhxy.hubu.edu.cn/rcpy/zsyjy/zsxx.htm) |
- | --------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------- |
- | rcpy | rcpy/bksjy | rcpy/yjsjy | rcpy/zsyjy/zsxx |
+| [人才培养](https://zhxy.hubu.edu.cn/rcpy.htm) | [本科生教育](https://zhxy.hubu.edu.cn/rcpy/bksjy.htm) | [研究生教育](https://zhxy.hubu.edu.cn/rcpy/yjsjy.htm) | [招生与就业](https://zhxy.hubu.edu.cn/rcpy/zsyjy/zsxx.htm) |
+| --------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------- |
+| rcpy | rcpy/bksjy | rcpy/yjsjy | rcpy/zsyjy/zsxx |
- #### [学科建设](https://zhxy.hubu.edu.cn/xkjianshe/zdxk.htm)
+#### [学科建设](https://zhxy.hubu.edu.cn/xkjianshe/zdxk.htm)
- | [学科建设](https://zhxy.hubu.edu.cn/xkjianshe/zdxk.htm) | [重点学科](https://zhxy.hubu.edu.cn/xkjianshe/zdxk.htm) | [硕士点](https://zhxy.hubu.edu.cn/xkjianshe/ssd.htm) | [博士点](https://zhxy.hubu.edu.cn/xkjianshe/bsd.htm) |
- | ------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- |
- | xkjianshe/zdxk | xkjianshe/zdxk | xkjianshe/ssd | xkjianshe/bsd |
+| [学科建设](https://zhxy.hubu.edu.cn/xkjianshe/zdxk.htm) | [重点学科](https://zhxy.hubu.edu.cn/xkjianshe/zdxk.htm) | [硕士点](https://zhxy.hubu.edu.cn/xkjianshe/ssd.htm) | [博士点](https://zhxy.hubu.edu.cn/xkjianshe/bsd.htm) |
+| ------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- |
+| xkjianshe/zdxk | xkjianshe/zdxk | xkjianshe/ssd | xkjianshe/bsd |
- #### [科研服务](https://zhxy.hubu.edu.cn/kyfw.htm)
+#### [科研服务](https://zhxy.hubu.edu.cn/kyfw.htm)
- | [科研服务](https://zhxy.hubu.edu.cn/kyfw.htm) | [科研动态](https://zhxy.hubu.edu.cn/kyfw/kydongt.htm) | [学术交流](https://zhxy.hubu.edu.cn/kyfw/xsjl.htm) | [科研平台](https://zhxy.hubu.edu.cn/kyfw/keyapt.htm) | [社会服务](https://zhxy.hubu.edu.cn/kyfw/shfuw.htm) |
- | --------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------- |
- | kyfw | kyfw/kydongt | kyfw/xsjl | kyfw/keyapt | kyfw/shfuw |
+| [科研服务](https://zhxy.hubu.edu.cn/kyfw.htm) | [科研动态](https://zhxy.hubu.edu.cn/kyfw/kydongt.htm) | [学术交流](https://zhxy.hubu.edu.cn/kyfw/xsjl.htm) | [科研平台](https://zhxy.hubu.edu.cn/kyfw/keyapt.htm) | [社会服务](https://zhxy.hubu.edu.cn/kyfw/shfuw.htm) |
+| --------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------- |
+| kyfw | kyfw/kydongt | kyfw/xsjl | kyfw/keyapt | kyfw/shfuw |
- #### [党群工作](https://zhxy.hubu.edu.cn/dqgz.htm)
+#### [党群工作](https://zhxy.hubu.edu.cn/dqgz.htm)
- | [党群工作](https://zhxy.hubu.edu.cn/dqgz.htm) | [党建工作](https://zhxy.hubu.edu.cn/dqgz/djgz/jgdj.htm) | [工会工作](https://zhxy.hubu.edu.cn/dqgz/ghgon.htm) |
- | --------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------- |
- | dqgz | dqgz/djgz/jgdj | dqgz/ghgon |
+| [党群工作](https://zhxy.hubu.edu.cn/dqgz.htm) | [党建工作](https://zhxy.hubu.edu.cn/dqgz/djgz/jgdj.htm) | [工会工作](https://zhxy.hubu.edu.cn/dqgz/ghgon.htm) |
+| --------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------- |
+| dqgz | dqgz/djgz/jgdj | dqgz/ghgon |
`,
categories: ['university'],
diff --git a/lib/routes/huggingface/blog-zh.ts b/lib/routes/huggingface/blog-zh.ts
index 732dd4f96009ee..df4117fb346bcc 100644
--- a/lib/routes/huggingface/blog-zh.ts
+++ b/lib/routes/huggingface/blog-zh.ts
@@ -47,7 +47,7 @@ async function handler() {
title: item.blog.title,
link: `https://huggingface.co${item.link}`,
category: item.blog.tags,
- pubDate: parseDate(item.blog.date),
+ pubDate: parseDate(item.blog.publishedAt),
author: item.blog.author,
}));
diff --git a/lib/routes/huggingface/blog.ts b/lib/routes/huggingface/blog.ts
new file mode 100644
index 00000000000000..71b4fc326fd50f
--- /dev/null
+++ b/lib/routes/huggingface/blog.ts
@@ -0,0 +1,87 @@
+import { Route, type DataItem } from '@/types';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+
+export const route: Route = {
+ path: '/blog',
+ categories: ['programming'],
+ example: '/huggingface/blog',
+ parameters: {},
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['huggingface.co/blog', 'huggingface.co/'],
+ },
+ ],
+ name: '英文博客',
+ maintainers: ['cesaryuan', 'zcf0508'],
+ handler,
+ url: 'huggingface.co/blog',
+};
+
+interface Author {
+ user: string;
+ guest: boolean;
+ org?: string;
+}
+
+interface Blog {
+ authors: Author[];
+ canonical: boolean;
+ isUpvotedByUser: boolean;
+ publishedAt: string;
+ slug: string;
+ title: string;
+ upvotes: number;
+ thumbnail: string;
+ guest: boolean;
+}
+
+interface BlogData {
+ blog: Blog;
+ blogUrl: string;
+ lang: string;
+ loggedInUser: string;
+}
+
+async function handler() {
+ const { body: response } = await got('https://huggingface.co/blog');
+ const $ = load(response);
+
+ /** @type {Array<{blog: {local: string, title: string, author: string, thumbnail: string, date: string, tags: Array更多分类
+
+#### [理论学习](https://mse.hust.edu.cn/llxx1.htm)
+
+| [党务动态](https://mse.hust.edu.cn/llxx1/dwdt/djxw.htm) | [共青团](https://mse.hust.edu.cn/llxx1/gqt/xwdt.htm) | [工会组织](https://mse.hust.edu.cn/llxx1/ghzz/xwgg.htm) | [学习参考](https://mse.hust.edu.cn/llxx1/xxck.htm) | [资料汇编](https://mse.hust.edu.cn/llxx1/zlhb.htm) | [其他群团](https://mse.hust.edu.cn/llxx1/ghzz1/lmmc.htm) |
+| -------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------- |
+| [llxx1/dwdt/djxw](https://rsshub.app/hust/mse/llxx1/dwdt/djxw) | [llxx1/gqt/xwdt](https://rsshub.app/hust/mse/llxx1/gqt/xwdt) | [llxx1/ghzz/xwgg](https://rsshub.app/hust/mse/llxx1/ghzz/xwgg) | [llxx1/xxck](https://rsshub.app/hust/mse/llxx1/xxck) | [llxx1/zlhb](https://rsshub.app/hust/mse/llxx1/zlhb) | [llxx1/ghzz1/lmmc](https://rsshub.app/hust/mse/llxx1/ghzz1/lmmc) |
+
+#### [师资队伍](https://mse.hust.edu.cn/szdw/jsml/jsml/qb.htm)
+
+| [人才招聘](https://mse.hust.edu.cn/szdw/rczp.htm) | [常用下载](https://mse.hust.edu.cn/szdw/cyxz.htm) |
+| -------------------------------------------------- | -------------------------------------------------- |
+| [szdw/rczp](https://rsshub.app/hust/mse/szdw/rczp) | [szdw/cyxz](https://rsshub.app/hust/mse/szdw/cyxz) |
+
+#### [人才培养](https://mse.hust.edu.cn/rcpy.htm)
+
+| [本科生教育](https://mse.hust.edu.cn/rcpy/bksjy.htm) | [研究生教育](https://mse.hust.edu.cn/rcpy/yjsjy.htm) | [学生工作](https://mse.hust.edu.cn/rcpy/xsg_z.htm) | [机械创新基地](https://mse.hust.edu.cn/rcpy/jxcxjd.htm) | [常用下载](https://mse.hust.edu.cn/rcpy/cyxz.htm) |
+| ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------- |
+| [rcpy/bksjy](https://rsshub.app/hust/mse/rcpy/bksjy) | [rcpy/yjsjy](https://rsshub.app/hust/mse/rcpy/yjsjy) | [rcpy/xsg_z](https://rsshub.app/hust/mse/rcpy/xsg_z) | [rcpy/jxcxjd](https://rsshub.app/hust/mse/rcpy/jxcxjd) | [rcpy/cyxz](https://rsshub.app/hust/mse/rcpy/cyxz) |
+
+#### [科学研究](https://mse.hust.edu.cn/kxyj.htm)
+
+| [科研动态](https://mse.hust.edu.cn/kxyj/kydt.htm) | [安全管理](https://mse.hust.edu.cn/kxyj/aqgl.htm) | [设备开放](https://mse.hust.edu.cn/kxyj/sbkf.htm) | [科研成果](https://mse.hust.edu.cn/kxyj/kycg.htm) | [常用下载](https://mse.hust.edu.cn/kxyj/cyxz.htm) |
+| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
+| [kxyj/kydt](https://rsshub.app/hust/mse/kxyj/kydt) | [kxyj/aqgl](https://rsshub.app/hust/mse/kxyj/aqgl) | [kxyj/sbkf](https://rsshub.app/hust/mse/kxyj/sbkf) | [kxyj/kycg](https://rsshub.app/hust/mse/kxyj/kycg) | [kxyj/cyxz](https://rsshub.app/hust/mse/kxyj/cyxz) |
+
+#### [社会服务](https://mse.hust.edu.cn/shfw.htm)
+
+| [驻外研究院](https://mse.hust.edu.cn/shfw/zwyjy.htm) | [产业公司](https://mse.hust.edu.cn/shfw/cygs.htm) |
+| ---------------------------------------------------- | -------------------------------------------------- |
+| [shfw/zwyjy](https://rsshub.app/hust/mse/shfw/zwyjy) | [shfw/cygs](https://rsshub.app/hust/mse/shfw/cygs) |
+
+#### [合作交流](https://mse.hust.edu.cn/hzjl.htm)
+
+| [专家来访](https://mse.hust.edu.cn/hzjl/zjlf.htm) | [师生出访](https://mse.hust.edu.cn/hzjl/sscf.htm) | [项目合作](https://mse.hust.edu.cn/hzjl/xmhz.htm) | [国际会议](https://mse.hust.edu.cn/hzjl/gjhy.htm) | [常用下载](https://mse.hust.edu.cn/hzjl/cyxz.htm) |
+| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
+| [hzjl/zjlf](https://rsshub.app/hust/mse/hzjl/zjlf) | [hzjl/sscf](https://rsshub.app/hust/mse/hzjl/sscf) | [hzjl/xmhz](https://rsshub.app/hust/mse/hzjl/xmhz) | [hzjl/gjhy](https://rsshub.app/hust/mse/hzjl/gjhy) | [hzjl/cyxz](https://rsshub.app/hust/mse/hzjl/cyxz) |
+
+#### [校友专栏](https://mse.hust.edu.cn/xyzl.htm)
+
+| [校友动态](https://mse.hust.edu.cn/xyzl/xydt.htm) | [杰出校友](https://mse.hust.edu.cn/xyzl/jcxy.htm) | [校友名录](https://mse.hust.edu.cn/xyzl/xyml.htm) | [校友照片](https://mse.hust.edu.cn/xyzl/xyzp.htm) | [服务校友](https://mse.hust.edu.cn/xyzl/fwxy.htm) | [常用下载](https://mse.hust.edu.cn/xyzl/cyxz.htm) |
+| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
+| [xyzl/xydt](https://rsshub.app/hust/mse/xyzl/xydt) | [xyzl/jcxy](https://rsshub.app/hust/mse/xyzl/jcxy) | [xyzl/xyml](https://rsshub.app/hust/mse/xyzl/xyml) | [xyzl/xyzp](https://rsshub.app/hust/mse/xyzl/xyzp) | [xyzl/fwxy](https://rsshub.app/hust/mse/xyzl/fwxy) | [xyzl/cyxz](https://rsshub.app/hust/mse/xyzl/cyxz) |
+
+#### [理论学习](https://mse.hust.edu.cn/sylm/xyxw.htm#)
+
+| [党务动态](https://mse.hust.edu.cn/llxx1/dwdt/djxw.htm) | [共青团](https://mse.hust.edu.cn/llxx1/gqt/xwdt.htm) | [工会组织](https://mse.hust.edu.cn/llxx1/ghzz/xwgg.htm) | [学习参考](https://mse.hust.edu.cn/llxx1/xxck.htm) | [资料汇编](https://mse.hust.edu.cn/llxx1/zlhb.htm) | [其他群团](https://mse.hust.edu.cn/llxx1/ghzz1/lmmc.htm) |
+| -------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------- |
+| [llxx1/dwdt/djxw](https://rsshub.app/hust/mse/llxx1/dwdt/djxw) | [llxx1/gqt/xwdt](https://rsshub.app/hust/mse/llxx1/gqt/xwdt) | [llxx1/ghzz/xwgg](https://rsshub.app/hust/mse/llxx1/ghzz/xwgg) | [llxx1/xxck](https://rsshub.app/hust/mse/llxx1/xxck) | [llxx1/zlhb](https://rsshub.app/hust/mse/llxx1/zlhb) | [llxx1/ghzz1/lmmc](https://rsshub.app/hust/mse/llxx1/ghzz1/lmmc) |
+
+#### [师资队伍](https://mse.hust.edu.cn/sylm/xyxw.htm#)
+
+| [人才招聘](https://mse.hust.edu.cn/szdw/rczp.htm) | [常用下载](https://mse.hust.edu.cn/szdw/cyxz.htm) |
+| -------------------------------------------------- | -------------------------------------------------- |
+| [szdw/rczp](https://rsshub.app/hust/mse/szdw/rczp) | [szdw/cyxz](https://rsshub.app/hust/mse/szdw/cyxz) |
+
+#### [人才培养](https://mse.hust.edu.cn/sylm/xyxw.htm#)
+
+| [本科生教育](https://mse.hust.edu.cn/rcpy/bksjy.htm) | [研究生教育](https://mse.hust.edu.cn/rcpy/yjsjy.htm) | [学生工作](https://mse.hust.edu.cn/rcpy/xsg_z.htm) | [机械创新基地](https://mse.hust.edu.cn/rcpy/jxcxjd.htm) | [常用下载](https://mse.hust.edu.cn/rcpy/cyxz.htm) |
+| ---------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------- |
+| [rcpy/bksjy](https://rsshub.app/hust/mse/rcpy/bksjy) | [rcpy/yjsjy](https://rsshub.app/hust/mse/rcpy/yjsjy) | [rcpy/xsg_z](https://rsshub.app/hust/mse/rcpy/xsg_z) | [rcpy/jxcxjd](https://rsshub.app/hust/mse/rcpy/jxcxjd) | [rcpy/cyxz](https://rsshub.app/hust/mse/rcpy/cyxz) |
+
+#### [科学研究](https://mse.hust.edu.cn/sylm/xyxw.htm#)
+
+| [科研动态](https://mse.hust.edu.cn/kxyj/kydt.htm) | [安全管理](https://mse.hust.edu.cn/kxyj/aqgl.htm) | [设备开放](https://mse.hust.edu.cn/kxyj/sbkf.htm) | [科研成果](https://mse.hust.edu.cn/kxyj/kycg.htm) | [常用下载](https://mse.hust.edu.cn/kxyj/cyxz.htm) |
+| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
+| [kxyj/kydt](https://rsshub.app/hust/mse/kxyj/kydt) | [kxyj/aqgl](https://rsshub.app/hust/mse/kxyj/aqgl) | [kxyj/sbkf](https://rsshub.app/hust/mse/kxyj/sbkf) | [kxyj/kycg](https://rsshub.app/hust/mse/kxyj/kycg) | [kxyj/cyxz](https://rsshub.app/hust/mse/kxyj/cyxz) |
+
+#### [社会服务](https://mse.hust.edu.cn/sylm/xyxw.htm#)
+
+| [驻外研究院](https://mse.hust.edu.cn/shfw/zwyjy.htm) | [产业公司](https://mse.hust.edu.cn/shfw/cygs.htm) |
+| ---------------------------------------------------- | -------------------------------------------------- |
+| [shfw/zwyjy](https://rsshub.app/hust/mse/shfw/zwyjy) | [shfw/cygs](https://rsshub.app/hust/mse/shfw/cygs) |
+
+#### [合作交流](https://mse.hust.edu.cn/sylm/xyxw.htm#)
+
+| [专家来访](https://mse.hust.edu.cn/hzjl/zjlf.htm) | [师生出访](https://mse.hust.edu.cn/hzjl/sscf.htm) | [项目合作](https://mse.hust.edu.cn/hzjl/xmhz.htm) | [国际会议](https://mse.hust.edu.cn/hzjl/gjhy.htm) | [常用下载](https://mse.hust.edu.cn/hzjl/cyxz.htm) |
+| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
+| [hzjl/zjlf](https://rsshub.app/hust/mse/hzjl/zjlf) | [hzjl/sscf](https://rsshub.app/hust/mse/hzjl/sscf) | [hzjl/xmhz](https://rsshub.app/hust/mse/hzjl/xmhz) | [hzjl/gjhy](https://rsshub.app/hust/mse/hzjl/gjhy) | [hzjl/cyxz](https://rsshub.app/hust/mse/hzjl/cyxz) |
+
+#### [校友专栏](https://mse.hust.edu.cn/sylm/xyxw.htm#)
+
+| [校友动态](https://mse.hust.edu.cn/xyzl/xydt.htm) | [杰出校友](https://mse.hust.edu.cn/xyzl/jcxy.htm) | [校友名录](https://mse.hust.edu.cn/xyzl/xyml.htm) | [校友照片](https://mse.hust.edu.cn/xyzl/xyzp.htm) | [服务校友](https://mse.hust.edu.cn/xyzl/fwxy.htm) | [常用下载](https://mse.hust.edu.cn/xyzl/cyxz.htm) |
+| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
+| [xyzl/xydt](https://rsshub.app/hust/mse/xyzl/xydt) | [xyzl/jcxy](https://rsshub.app/hust/mse/xyzl/jcxy) | [xyzl/xyml](https://rsshub.app/hust/mse/xyzl/xyml) | [xyzl/xyzp](https://rsshub.app/hust/mse/xyzl/xyzp) | [xyzl/fwxy](https://rsshub.app/hust/mse/xyzl/fwxy) | [xyzl/cyxz](https://rsshub.app/hust/mse/xyzl/cyxz) |
+
+
'),
+ link: `https://ak.hypergryph.com/news/${item.cid}`,
+ pubDate: parseDate(item.displayTime, 'X'),
+ }));
+
+export const route: Route = {
+ path: '/arknights/news/:group?',
+ categories: ['game'],
+ example: '/hypergryph/arknights/news',
+ parameters: { group: '分组,默认为 `ALL`' },
+ radar: [
+ {
+ source: ['ak-conf.hypergryph.com/news'],
+ },
+ ],
+ name: '明日方舟 - 游戏公告与新闻',
+ maintainers: ['Astrian'],
+ handler,
+ url: 'ak-conf.hypergryph.com/news',
+ description: `
+| 全部 | 最新 | 公告 | 活动 | 新闻 |
+| ---- | ------ | ------------ | -------- | ---- |
+| ALL | LATEST | ANNOUNCEMENT | ACTIVITY | NEWS |`,
+};
+
+async function handler(ctx) {
+ const { group = 'ALL' } = ctx.req.param();
+
+ const initialData: Promise`;
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: 'NEWS | アイドルマスター',
+ link: 'https://idolmaster-official.jp/news',
+ item: items,
+ language: 'ja',
+ };
+}
+
+function toUpperCase(input: string | string[] | undefined): string | string[] | undefined {
+ if (!input) {
+ return input;
+ }
+ return typeof input === 'string' ? input.toUpperCase() : input.map((item) => item.toUpperCase());
+}
diff --git a/lib/routes/idolypride/namespace.ts b/lib/routes/idolypride/namespace.ts
index fccb01ceb045dd..42a5d7318f5dc6 100644
--- a/lib/routes/idolypride/namespace.ts
+++ b/lib/routes/idolypride/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'IDOLY PRIDE 偶像荣耀',
url: 'idolypride.jp',
+ lang: 'ja',
};
diff --git a/lib/routes/idolypride/news.ts b/lib/routes/idolypride/news.ts
index 2bd302d918048b..2cdc1a13ea683c 100644
--- a/lib/routes/idolypride/news.ts
+++ b/lib/routes/idolypride/news.ts
@@ -1,11 +1,12 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
export const route: Route = {
path: '/news',
- categories: ['anime'],
+ categories: ['anime', 'popular'],
+ view: ViewType.Articles,
example: '/idolypride/news',
parameters: {},
features: {
diff --git a/lib/routes/ieee-security/namespace.ts b/lib/routes/ieee-security/namespace.ts
index 4c0a0af48a68aa..751de609c138fd 100644
--- a/lib/routes/ieee-security/namespace.ts
+++ b/lib/routes/ieee-security/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'IEEE Computer Society',
url: 'ieee-security.org',
+ lang: 'en',
};
diff --git a/lib/routes/ieee/author.ts b/lib/routes/ieee/author.ts
new file mode 100644
index 00000000000000..78f1a8001bee69
--- /dev/null
+++ b/lib/routes/ieee/author.ts
@@ -0,0 +1,95 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch';
+import { load } from 'cheerio';
+import path from 'node:path';
+import { parseDate } from '@/utils/parse-date';
+import { art } from '@/utils/render';
+import { getCurrentPath } from '@/utils/helpers';
+const __dirname = getCurrentPath(import.meta.url);
+
+export const route: Route = {
+ name: 'IEEE Author Articles',
+ maintainers: ['Derekmini'],
+ categories: ['journal'],
+ path: '/author/:aid/:sortType',
+ parameters: {
+ aid: 'Author ID',
+ sortType: 'Sort Type of papers',
+ },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: true,
+ },
+ example: '/ieee/author/37264968900/newest',
+ handler,
+};
+
+async function handler(ctx) {
+ const { aid, sortType } = ctx.req.param();
+ const count = ctx.req.query('limit') || 10;
+ const host = 'https://ieeexplore.ieee.org';
+
+ const res = await ofetch(`${host}/rest/author/${aid}`);
+ const author = res[0];
+ const title = `${author.preferredName} on IEEE Xplore`;
+ const link = `${host}/author/${aid}`;
+ const description = author.bioParagraphs.join(' ');
+ const image = `${host}${author.photoUrl}`;
+
+ const response = await ofetch(`${host}/rest/search`, {
+ method: 'POST',
+ body: {
+ rowsPerPage: count,
+ searchWithin: [`"Author Ids": ${aid}`],
+ sortType,
+ },
+ });
+
+ const list = response.records.map((item) => ({
+ title: item.articleTitle,
+ link: item.htmlLink,
+ doi: item.doi,
+ authors: 'authors' in item ? item.authors.map((itemAuth) => itemAuth.preferredName).join('; ') : 'Do not have author',
+ abstract: 'abstract' in item ? item.abstract : '',
+ }));
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ if (item.abstract !== '') {
+ const res = await ofetch(`${host}${item.link}`, {
+ parseResponse: (txt) => txt,
+ });
+ const $ = load(res);
+ const metadataMatch = $.html().match(/metadata=(.*);/);
+ const metadata = metadataMatch ? JSON.parse(metadataMatch[1]) : null;
+ item.pubDate = metadata?.insertDate ? parseDate(metadata.insertDate) : undefined;
+ item.abstract = load(metadata?.abstract || '').text();
+ }
+ return {
+ ...item,
+ description: renderDescription(item),
+ };
+ })
+ )
+ );
+
+ return {
+ title,
+ link,
+ description,
+ item: items,
+ image,
+ };
+}
+
+function renderDescription(item: { title: string; authors: string; abstract: string; doi: string }) {
+ return art(path.join(__dirname, 'templates/description.art'), {
+ item,
+ });
+}
diff --git a/lib/routes/ieee/earlyaccess.ts b/lib/routes/ieee/earlyaccess.ts
deleted file mode 100644
index 9f18bc0b02d974..00000000000000
--- a/lib/routes/ieee/earlyaccess.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { Route } from '@/types';
-import { getCurrentPath } from '@/utils/helpers';
-const __dirname = getCurrentPath(import.meta.url);
-
-import cache from '@/utils/cache';
-import got from '@/utils/got';
-import { load } from 'cheerio';
-import path from 'node:path';
-import { art } from '@/utils/render';
-
-import { CookieJar } from 'tough-cookie';
-const cookieJar = new CookieJar();
-
-export const route: Route = {
- path: '/journal/:journal/earlyaccess/:sortType?',
- categories: ['journal'],
- example: '/ieee/journal/5306045/earlyaccess',
- parameters: { journal: 'Issue code, the number of the `isnumber` in the URL', sortType: 'Sort Type, default: `vol-only-seq`, the part of the URL after `sortType`' },
- features: {
- requireConfig: false,
- requirePuppeteer: false,
- antiCrawler: false,
- supportBT: false,
- supportPodcast: false,
- supportScihub: false,
- },
- name: 'Early Access Journal',
- maintainers: ['5upernova-heng'],
- handler,
-};
-
-async function handler(ctx) {
- const isnumber = ctx.req.param('journal');
- const sortType = ctx.req.param('sortType') ?? 'vol-only-seq';
- const host = 'https://ieeexplore.ieee.org';
- const jrnlUrl = `${host}/xpl/tocresult.jsp?isnumber=${isnumber}`;
-
- const response = await got(`${host}/rest/publication/home/metadata?issueid=${isnumber}`, {
- cookieJar,
- }).json();
- const punumber = response.publicationNumber;
- const volume = response.currentIssue.volume;
- const jrnlName = response.displayTitle;
-
- const response2 = await got
- .post(`${host}/rest/search/pub/${punumber}/issue/${isnumber}/toc`, {
- cookieJar,
- json: {
- punumber,
- isnumber,
- sortType,
- rowsPerPage: '100',
- },
- })
- .json();
- let list = response2.records.map((item) => {
- const $2 = load(item.articleTitle);
- const title = $2.text();
- const link = item.htmlLink;
- const doi = item.doi;
- let authors = 'Do not have author';
- if (Object.hasOwn(item, 'authors')) {
- authors = item.authors.map((itemAuth) => itemAuth.preferredName).join('; ');
- }
- const abstract = Object.hasOwn(item, 'abstract') ? item.abstract : '';
- return {
- title,
- link,
- authors,
- doi,
- volume,
- abstract,
- };
- });
-
- const renderDesc = (item) =>
- art(path.join(__dirname, 'templates/description.art'), {
- item,
- });
- list = await Promise.all(
- list.map((item) =>
- cache.tryGet(item.link, async () => {
- if (item.abstract !== '') {
- const response3 = await got(`${host}${item.link}`);
- const { abstract } = JSON.parse(response3.body.match(/metadata=(.*);/)[1]);
- const $3 = load(abstract);
- item.abstract = $3.text();
- item.description = renderDesc(item);
- }
- return item;
- })
- )
- );
-
- return {
- title: jrnlName,
- link: jrnlUrl,
- item: list,
- };
-}
diff --git a/lib/routes/ieee/journal.ts b/lib/routes/ieee/journal.ts
index f8e38085b9aa6c..c8ffa34713796b 100644
--- a/lib/routes/ieee/journal.ts
+++ b/lib/routes/ieee/journal.ts
@@ -2,89 +2,72 @@ import { Route } from '@/types';
import { getCurrentPath } from '@/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);
-import cache from '@/utils/cache';
import got from '@/utils/got';
-import { load } from 'cheerio';
import path from 'node:path';
import { art } from '@/utils/render';
-import { CookieJar } from 'tough-cookie';
-const cookieJar = new CookieJar();
+const ieeeHost = 'https://ieeexplore.ieee.org';
export const route: Route = {
- path: ['/:journal/latest/vol/:sortType?', '/journal/:journal/:sortType?'],
- name: 'Unknown',
- maintainers: [],
+ name: 'IEEE Journal Articles',
+ maintainers: ['HenryQW'],
+ categories: ['journal'],
+ path: '/journal/:punumber/:earlyAccess?',
+ parameters: {
+ punumber: 'Publication Number, look for `punumber` in the URL',
+ earlyAccess: 'Optional, set any value to get early access articles',
+ },
+ example: '/ieee/journal/6287639/preprint',
handler,
};
async function handler(ctx) {
- const punumber = ctx.req.param('journal');
- const sortType = ctx.req.param('sortType') ?? 'vol-only-seq';
- const host = 'https://ieeexplore.ieee.org';
- const jrnlUrl = `${host}/xpl/mostRecentIssue.jsp?punumber=${punumber}`;
+ const publicationNumber = ctx.req.param('punumber');
+ const earlyAccess = !!ctx.req.param('earlyAccess');
- const response = await got(`${host}/rest/publication/home/metadata?pubid=${punumber}`, {
- cookieJar,
- }).json();
- const volume = response.currentIssue.volume;
- const isnumber = response.currentIssue.issueNumber;
- const jrnlName = response.displayTitle;
+ const metadata = await fetchMetadata(publicationNumber);
+ const { displayTitle, currentIssue, preprintIssue, coverImagePath } = metadata;
+ const { issueNumber, volume } = earlyAccess ? preprintIssue : currentIssue;
- const response2 = await got
- .post(`${host}/rest/search/pub/${punumber}/issue/${isnumber}/toc`, {
- cookieJar,
- json: {
- punumber,
- isnumber,
- sortType,
- rowsPerPage: '100',
- },
- })
- .json();
- let list = response2.records.map((item) => {
- const $2 = load(item.articleTitle);
- const title = $2.text();
- const link = item.htmlLink;
- const doi = item.doi;
- let authors = 'Do not have author';
- if (Object.hasOwn(item, 'authors')) {
- authors = item.authors.map((itemAuth) => itemAuth.preferredName).join('; ');
- }
- let abstract = '';
- Object.hasOwn(item, 'abstract') ? (abstract = item.abstract) : (abstract = '');
- return {
- title,
- link,
- authors,
- doi,
- volume,
- abstract,
- };
- });
+ const tocData = await fetchTOCData(publicationNumber, issueNumber);
+ const list = tocData.records.map((item) => {
+ const mappedItem = mapRecordToItem(volume)(item);
- const renderDesc = (item) =>
- art(path.join(__dirname, 'templates/description.art'), {
- item,
+ mappedItem.description = art(path.join(__dirname, 'templates/description.art'), {
+ item: mappedItem,
});
- list = await Promise.all(
- list.map((item) =>
- cache.tryGet(item.link, async () => {
- if (item.abstract !== '') {
- const response3 = await got(`${host}${item.link}`);
- const { abstract } = JSON.parse(response3.body.match(/metadata=(.*);/)[1]);
- const $3 = load(abstract);
- item.abstract = $3.text();
- item.description = renderDesc(item);
- }
- return item;
- })
- )
- );
+
+ return mappedItem;
+ });
return {
- title: jrnlName,
- link: jrnlUrl,
+ title: displayTitle,
+ link: `${ieeeHost}/xpl/tocresult.jsp?isnumber=${issueNumber}`,
item: list,
+ image: `${ieeeHost}${coverImagePath}`,
};
}
+
+async function fetchMetadata(punumber) {
+ const response = await got(`${ieeeHost}/rest/publication/home/metadata?pubid=${punumber}`);
+ return response.data;
+}
+
+async function fetchTOCData(punumber, isnumber) {
+ const response = await got.post(`${ieeeHost}/rest/search/pub/${punumber}/issue/${isnumber}/toc`, {
+ json: { punumber, isnumber, rowsPerPage: '100' },
+ });
+ return response.data;
+}
+
+function mapRecordToItem(volume) {
+ return (item) => ({
+ abstract: item.abstract || '',
+ authors: item.authors ? item.authors.map((author) => author.preferredName).join('; ') : '',
+ description: '',
+ doi: item.doi,
+ link: item.htmlLink,
+ title: item.articleTitle || '',
+ volume,
+ });
+}
diff --git a/lib/routes/ieee/namespace.ts b/lib/routes/ieee/namespace.ts
index 67a0f94020fa0d..a56a5a87dd3d12 100644
--- a/lib/routes/ieee/namespace.ts
+++ b/lib/routes/ieee/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'IEEE Xplore',
url: 'www.ieee.org',
+ lang: 'en',
};
diff --git a/lib/routes/ieee/recent.ts b/lib/routes/ieee/recent.ts
deleted file mode 100644
index b5f75bc7eef528..00000000000000
--- a/lib/routes/ieee/recent.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { Route } from '@/types';
-import { getCurrentPath } from '@/utils/helpers';
-const __dirname = getCurrentPath(import.meta.url);
-
-import cache from '@/utils/cache';
-import got from '@/utils/got';
-import { load } from 'cheerio';
-import path from 'node:path';
-import { art } from '@/utils/render';
-
-import { CookieJar } from 'tough-cookie';
-const cookieJar = new CookieJar();
-
-export const route: Route = {
- path: ['/:journal/latest/date/:sortType?', '/journal/:journal/recent/:sortType?'],
- name: 'Unknown',
- maintainers: [],
- handler,
-};
-
-async function handler(ctx) {
- const punumber = ctx.req.param('journal');
- const sortType = ctx.req.param('sortType') ?? 'vol-only-seq';
- const host = 'https://ieeexplore.ieee.org';
- const jrnlUrl = `${host}/xpl/mostRecentIssue.jsp?punumber=${punumber}`;
-
- const date = new Date();
- const year = date.getFullYear();
- const month = date.getMonth() + 1;
- let strYM, endYM;
- const snap = 2;
- if (1 <= month && month <= snap) {
- month - snap + 12 < 10 ? (strYM = year - 1 + '0' + (month - snap + 12)) : (strYM = year - 1 + '' + (month - snap + 12));
- endYM = year + '0' + month;
- } else if (snap < month && month < 10) {
- month - snap < 10 ? (strYM = year + '0' + (month - snap)) : (strYM = year + '' + (month - snap));
- endYM = year + '0' + month;
- } else {
- month - snap < 10 ? (strYM = year + '0' + (month - snap)) : (strYM = year + '' + (month - snap));
- endYM = year + '' + month;
- }
-
- const response = await got(`${host}/rest/publication/home/metadata?pubid=${punumber}`, {
- cookieJar,
- }).json();
- const volume = response.currentIssue.volume;
- const isnumber = response.currentIssue.issueNumber;
- const jrnlName = response.displayTitle;
-
- const response2 = await got
- .post(`${host}/rest/search/pub/${punumber}/issue/${isnumber}/toc`, {
- cookieJar,
- json: {
- punumber,
- isnumber,
- sortType,
- rowsPerPage: '100',
- ranges: [strYM + `01_` + endYM + `31_Search Latest Date`],
- },
- })
- .json();
- let list = response2.records.map((item) => {
- const $2 = load(item.articleTitle);
- const title = $2.text();
- const link = item.htmlLink;
- const doi = item.doi;
- let authors = 'Do not have author';
- if (Object.hasOwn(item, 'authors')) {
- authors = item.authors.map((itemAuth) => itemAuth.preferredName).join('; ');
- }
- let abstract = '';
- Object.hasOwn(item, 'abstract') ? (abstract = item.abstract) : (abstract = '');
- return {
- title,
- link,
- authors,
- doi,
- volume,
- abstract,
- };
- });
-
- const renderDesc = (item) =>
- art(path.join(__dirname, 'templates/description.art'), {
- item,
- });
- list = await Promise.all(
- list.map((item) =>
- cache.tryGet(item.link, async () => {
- if (item.abstract !== '') {
- const response3 = await got(`${host}${item.link}`);
- const { abstract } = JSON.parse(response3.body.match(/metadata=(.*);/)[1]);
- const $3 = load(abstract);
- item.abstract = $3.text();
- item.description = renderDesc(item);
- }
- return item;
- })
- )
- );
-
- return {
- title: `${jrnlName} - Recent`,
- link: jrnlUrl,
- item: list,
- };
-}
diff --git a/lib/routes/ieee/templates/description.art b/lib/routes/ieee/templates/description.art
index 04367cc08d3da2..a9e8c5da222192 100644
--- a/lib/routes/ieee/templates/description.art
+++ b/lib/routes/ieee/templates/description.art
@@ -3,7 +3,7 @@
- https://doi.org/{{ item.doi }}
+ https://doi.org/{{ item.doi }}
Volume {{ item.volume }}
`;
+ }
+ description += item.post_content;
+
+ return {
+ title: item.post_title.trim(),
+ description,
+ link: item.post_url,
+ pubDate: parseDate(item.published_at * 1000),
+ author: item.created_by.name,
+ };
+ })
+ );
+
+ return {
+ title: `#${name} - iFanr 爱范儿`,
+ link: `https://www.ifanr.com/category/${PATH_LIST[name]}`,
+ description: `${name} 更新推送`,
+ item: items,
+ };
+}
diff --git a/lib/routes/ifanr/digest.ts b/lib/routes/ifanr/digest.ts
new file mode 100644
index 00000000000000..12d4c77428522f
--- /dev/null
+++ b/lib/routes/ifanr/digest.ts
@@ -0,0 +1,104 @@
+import { type CheerioAPI, load } from 'cheerio';
+import { type Context } from 'hono';
+
+import { type DataItem, type Route, type Data, ViewType } from '@/types';
+
+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') ?? '20', 10);
+
+ const baseUrl: string = 'https://www.ifanr.com';
+ const apiBaseUrl: string = 'https://sso.ifanr.com';
+ const targetUrl: string = new URL('digest', baseUrl).href;
+ const apiUrl: string = new URL('api/v5/wp/buzz', apiBaseUrl).href;
+
+ const response = await ofetch(apiUrl, {
+ query: {
+ limit,
+ offset: 0,
+ },
+ });
+
+ const targetResponse = await ofetch(targetUrl);
+ const $: CheerioAPI = load(targetResponse);
+ const language: string = $('html').attr('lang') ?? 'zh-CN';
+
+ const items: DataItem[] = response.objects.slice(0, limit).map((item): DataItem => {
+ const title: string = item.post_title;
+ const description: string = item.post_content;
+ const pubDate: number | string = item.created_at;
+ const linkUrl: string = `digest/${item.post_id}`;
+ const guid: string = `ifanr-digest-${item.post_id}`;
+ const updated: number | string = item.updated_at ?? pubDate;
+
+ const processedItem: DataItem = {
+ title,
+ description,
+ pubDate: pubDate ? parseDate(pubDate, 'X') : undefined,
+ link: new URL(linkUrl, baseUrl).href,
+ guid,
+ id: guid,
+ content: {
+ html: description,
+ text: item.post_content ?? description,
+ },
+ updated: updated ? parseDate(updated) : undefined,
+ language,
+ _extra: {
+ links: [
+ {
+ url: item.buzz_original_url,
+ type: 'via',
+ content_html: item.post_content,
+ },
+ ],
+ },
+ };
+
+ return processedItem;
+ });
+
+ const title: string = $('title').text();
+
+ return {
+ title,
+ description: title,
+ link: targetUrl,
+ item: items,
+ allowEmpty: true,
+ image: $('img.c-header-navbar__logo').attr('src'),
+ author: $('meta[property="og:site_name"]').attr('content'),
+ language,
+ id: $('meta[property="og:url"]').attr('content'),
+ };
+};
+
+export const route: Route = {
+ path: '/digest',
+ name: '快讯',
+ url: 'www.ifanr.com',
+ maintainers: ['nczitzk'],
+ handler,
+ example: '/ifanr/digest',
+ parameters: undefined,
+ description: undefined,
+ categories: ['new-media', 'popular'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['www.ifanr.comdigest'],
+ target: '/digest',
+ },
+ ],
+ view: ViewType.Articles,
+};
diff --git a/lib/routes/ifanr/index.ts b/lib/routes/ifanr/index.ts
new file mode 100644
index 00000000000000..be1fe674821f1b
--- /dev/null
+++ b/lib/routes/ifanr/index.ts
@@ -0,0 +1,70 @@
+import { Route, ViewType } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { parseDate } from '@/utils/parse-date';
+
+export const route: Route = {
+ path: '/index',
+ categories: ['new-media', 'popular'],
+ view: ViewType.Articles,
+ example: '/ifanr/index',
+ parameters: {},
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['www.ifanr.com/index'],
+ },
+ ],
+ name: '首页',
+ maintainers: ['donghongfei'],
+ handler,
+ url: 'www.ifanr.com/index',
+};
+
+async function handler() {
+ const apiUrl = 'https://sso.ifanr.com/api/v5/wp/web-feed/?limit=20&offset=0';
+ const resp = await got({
+ method: 'get',
+ url: apiUrl,
+ });
+ const items = await Promise.all(
+ resp.data.objects.map((item) => {
+ const link = `https://sso.ifanr.com/api/v5/wp/article/?post_id=${item.post_id}`;
+ let description = '';
+
+ const key = `ifanr:${item.id}`;
+
+ return cache.tryGet(key, async () => {
+ const response = await got({ method: 'get', url: link });
+ const articleData = response.data.objects[0];
+ const banner = articleData.post_cover_image;
+ if (banner) {
+ description = `
`;
+ }
+ description += articleData.post_content;
+
+ return {
+ title: item.post_title.trim(),
+ description,
+ link: item.post_url,
+ pubDate: parseDate(item.created_at * 1000),
+ author: item.created_by.name,
+ };
+ });
+ })
+ );
+
+ return {
+ title: '爱范儿',
+ link: 'https://www.ifanr.com',
+ description: '爱范儿首页',
+ item: items,
+ };
+}
diff --git a/lib/routes/ifanr/namespace.ts b/lib/routes/ifanr/namespace.ts
new file mode 100644
index 00000000000000..b06f5927e11adf
--- /dev/null
+++ b/lib/routes/ifanr/namespace.ts
@@ -0,0 +1,8 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '爱范儿',
+ url: 'www.ifanr.com',
+ description: '',
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/ifeng/feng.ts b/lib/routes/ifeng/feng.ts
index f076710f64a39c..fc59a2f9abb262 100644
--- a/lib/routes/ifeng/feng.ts
+++ b/lib/routes/ifeng/feng.ts
@@ -23,8 +23,8 @@ export const route: Route = {
maintainers: ['Jamch'],
handler,
description: `| 文章 | 视频 |
- | ---- | ----- |
- | doc | video |`,
+| ---- | ----- |
+| doc | video |`,
};
async function handler(ctx) {
diff --git a/lib/routes/ifeng/namespace.ts b/lib/routes/ifeng/namespace.ts
index eb086ad00c2466..fc92d29eefc725 100644
--- a/lib/routes/ifeng/namespace.ts
+++ b/lib/routes/ifeng/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '凤凰网',
url: 'feng.ifeng.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/ifi-audio/download.ts b/lib/routes/ifi-audio/download.ts
index 6419707f2c554e..5b82846a85c007 100644
--- a/lib/routes/ifi-audio/download.ts
+++ b/lib/routes/ifi-audio/download.ts
@@ -20,7 +20,7 @@ export const route: Route = {
name: 'Download Hub',
maintainers: ['EthanWng97'],
handler,
- description: `:::warning
+ description: `::: warning
1. Open [https://ifi-audio.com/download-hub](https://ifi-audio.com/download-hub) and the Network panel
2. Select the device and the corresponding serial number in the website and click Search
3. Find the last request named \`https://ifi-audio.com/wp-admin/admin-ajax.php\` in the Network panel, find out the val and id in the Payload panel, and fill in the url
diff --git a/lib/routes/ifi-audio/namespace.ts b/lib/routes/ifi-audio/namespace.ts
index 2caf35ab4c79ca..1287bea5b36e85 100644
--- a/lib/routes/ifi-audio/namespace.ts
+++ b/lib/routes/ifi-audio/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'iFi audio',
url: 'ifi-audio.com',
+ lang: 'en',
};
diff --git a/lib/routes/ifun/n/category.ts b/lib/routes/ifun/n/category.ts
new file mode 100644
index 00000000000000..b7f78de1fabeaa
--- /dev/null
+++ b/lib/routes/ifun/n/category.ts
@@ -0,0 +1,102 @@
+import { type Context } from 'hono';
+
+import { type DataItem, type Route, type Data, ViewType } from '@/types';
+
+import ofetch from '@/utils/ofetch';
+
+import { author, language, rootUrl, processItems } from './util';
+
+export const handler = async (ctx: Context): Promise => {
+ const { id } = ctx.req.param();
+ const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10);
+
+ const targetUrl: string = rootUrl;
+ const apiUrl: string = new URL(`api/articles/${id ? 'categoryId' : 'all'}`, rootUrl).href;
+ const apiCategoryUrl: string = new URL('api/categories/all', rootUrl).href;
+
+ const apiResponse = await ofetch(apiUrl, {
+ query: {
+ datasrc: id ? 'categoriesall' : 'articles',
+ current: 1,
+ size: limit,
+ categoryId: id,
+ },
+ });
+
+ const apiCategoryResponse = await ofetch(apiCategoryUrl, {
+ query: {
+ datasrc: 'categories',
+ },
+ });
+
+ const categoryName: string = apiCategoryResponse.data.find((item) => item.categoryid === id)?.category;
+
+ const items: DataItem[] = processItems(apiResponse.data.records, limit);
+
+ return {
+ title: `${author}${categoryName ? ` - ${categoryName}` : ''}`,
+ description: categoryName,
+ link: targetUrl,
+ item: items,
+ allowEmpty: true,
+ author,
+ language,
+ };
+};
+
+export const route: Route = {
+ path: '/n/category/:id?',
+ name: '盐选故事分类',
+ url: 'n.ifun.cool',
+ maintainers: ['nczitzk'],
+ handler,
+ example: '/ifun/n/category',
+ parameters: {
+ id: '分类 id,默认为空,即全部,见下表',
+ },
+ description: `
+| 名称 | ID |
+| -------- | --- |
+| 全部 | |
+| 通告 | 1 |
+| 故事盐选 | 2 |
+| 趣集精选 | 3 |
+ `,
+ categories: ['new-media'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['n.ifun.cool'],
+ target: '/n/category/:id?',
+ },
+ {
+ title: '全部',
+ source: ['n.ifun.cool'],
+ target: '/n/category',
+ },
+ {
+ title: '通告',
+ source: ['n.ifun.cool'],
+ target: '/n/category/1',
+ },
+ {
+ title: '盐选故事',
+ source: ['n.ifun.cool'],
+ target: '/n/category/2',
+ },
+ {
+ title: '趣集精选',
+ source: ['n.ifun.cool'],
+ target: '/n/category/3',
+ },
+ ],
+ view: ViewType.Articles,
+};
diff --git a/lib/routes/ifun/n/search.ts b/lib/routes/ifun/n/search.ts
new file mode 100644
index 00000000000000..bb0ca4772dc841
--- /dev/null
+++ b/lib/routes/ifun/n/search.ts
@@ -0,0 +1,73 @@
+import { type Context } from 'hono';
+
+import { type DataItem, type Route, type Data, ViewType } from '@/types';
+
+import ofetch from '@/utils/ofetch';
+
+import { author, language, rootUrl, processItems } from './util';
+
+export const handler = async (ctx: Context): Promise => {
+ const { keywords } = ctx.req.param();
+ const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10);
+
+ const targetUrl: string = new URL(`search-result/?s=${keywords}`, rootUrl).href;
+ const apiUrl: string = new URL('api/articles/searchkeywords', rootUrl).href;
+
+ const apiResponse = await ofetch(apiUrl, {
+ query: {
+ keywords,
+ current: 1,
+ size: limit,
+ },
+ });
+
+ const items: DataItem[] = processItems(apiResponse.data.records, limit);
+
+ return {
+ title: `${author} - ${keywords}`,
+ description: keywords,
+ link: targetUrl,
+ item: items,
+ allowEmpty: true,
+ author,
+ language,
+ };
+};
+
+export const route: Route = {
+ path: '/n/search/:keywords',
+ name: '盐选故事搜索',
+ url: 'n.ifun.cool',
+ maintainers: ['nczitzk'],
+ handler,
+ example: '/ifun/n/search/NPC',
+ parameters: {
+ keywords: '搜索关键字',
+ },
+ description: `::: tip
+若订阅 [关键词:NPC](https://n.ifun.cool/search-result/?s=NPC),网址为 \`https://n.ifun.cool/search-result/?s=NPC\`,请截取 \`s\` 的值 \`NPC\` 作为 \`keywords\` 参数填入,此时目标路由为 [\`/ifun/n/search/NPC\`](https://rsshub.app/ifun/n/search/NPC)。
+:::
+ `,
+ categories: ['new-media'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['n.ifun.cool/search-result'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const keywords = urlObj.searchParams.get('s');
+
+ return `/ifun/n/search/${keywords}`;
+ },
+ },
+ ],
+ view: ViewType.Articles,
+};
diff --git a/lib/routes/ifun/n/tag.ts b/lib/routes/ifun/n/tag.ts
new file mode 100644
index 00000000000000..0577c40fccb1d4
--- /dev/null
+++ b/lib/routes/ifun/n/tag.ts
@@ -0,0 +1,76 @@
+import { type Context } from 'hono';
+
+import { type DataItem, type Route, type Data, ViewType } from '@/types';
+
+import ofetch from '@/utils/ofetch';
+
+import { author, language, rootUrl, processItems } from './util';
+
+export const handler = async (ctx: Context): Promise => {
+ const { name } = ctx.req.param();
+ const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10);
+
+ const targetUrl: string = new URL(`article-list/1?tagName=${name}`, rootUrl).href;
+ const apiUrl: string = new URL('api/articles/tagId', rootUrl).href;
+
+ const apiResponse = await ofetch(apiUrl, {
+ query: {
+ datasrc: 'tagid',
+ tagname: name,
+ current: 1,
+ size: limit,
+ },
+ });
+
+ const items: DataItem[] = processItems(apiResponse.data.records, limit);
+
+ return {
+ title: `${author} - ${name}`,
+ description: name,
+ link: targetUrl,
+ item: items,
+ allowEmpty: true,
+ author,
+ language,
+ };
+};
+
+export const route: Route = {
+ path: '/n/tag/:name',
+ name: '盐选故事专栏',
+ url: 'n.ifun.cool',
+ maintainers: ['nczitzk'],
+ handler,
+ example: '/ifun/n/tag/zhihu',
+ parameters: {
+ name: '专栏 id,可在对应专栏页 URL 中找到',
+ },
+ description: `::: tip
+若订阅 [zhihu](https://n.ifun.cool/article-list/2?tagName=zhihu),网址为 \`https://n.ifun.cool/article-list/2?tagName=zhihu\`,请截取 \`tagName\` 的值 \`zhihu\` 作为 \`name\` 参数填入,此时目标路由为 [\`/ifun/n/tag/zhihu\`](https://rsshub.app/ifun/n/tag/zhihu)。
+
+更多专栏请见 [盐选故事专栏](https://n.ifun.cool/tags)。
+:::
+ `,
+ categories: ['new-media'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['n.ifun.cool/article-list/1'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const name = urlObj.searchParams.get('tagName');
+
+ return `/ifun/n/tag/${name}`;
+ },
+ },
+ ],
+ view: ViewType.Articles,
+};
diff --git a/lib/routes/ifun/n/util.ts b/lib/routes/ifun/n/util.ts
new file mode 100644
index 00000000000000..5f1bfa14d85913
--- /dev/null
+++ b/lib/routes/ifun/n/util.ts
@@ -0,0 +1,34 @@
+import { type DataItem } from '@/types';
+
+import { parseDate } from '@/utils/parse-date';
+
+const author: string = '趣集';
+const language: string = 'zh-CN';
+const rootUrl: string = 'https://n.ifun.cool';
+
+const processItems: (items: any[], limit: number) => DataItem[] = (items: any[], limit: number) =>
+ items.slice(0, limit).map((item): DataItem => {
+ const title: string = item.title;
+ const description: string = item.content;
+ const guid: string = `ifun-n-${item.id}`;
+
+ const author: DataItem['author'] = item.author;
+
+ return {
+ title,
+ description,
+ pubDate: parseDate(item.createtime),
+ link: item.id ? new URL(`articles/${item.id}`, rootUrl).href : undefined,
+ category: [...new Set([item.category, item.tag].filter(Boolean))],
+ author,
+ guid,
+ id: guid,
+ content: {
+ html: description,
+ text: description,
+ },
+ language,
+ };
+ });
+
+export { author, language, rootUrl, processItems };
diff --git a/lib/routes/ifun/namespace.ts b/lib/routes/ifun/namespace.ts
new file mode 100644
index 00000000000000..2fa5dca071e641
--- /dev/null
+++ b/lib/routes/ifun/namespace.ts
@@ -0,0 +1,9 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '趣集',
+ url: 'ifun.cool',
+ categories: ['new-media'],
+ description: '全面的找书、学习资源导航平台,它整合了电子书和科研文档的搜索功能,方便用户进行学习资料的检索和分享,为用户提供一站式的读书学习体验。',
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/iguoguo/namespace.ts b/lib/routes/iguoguo/namespace.ts
index b05cd766cc0cba..7ce4315b001a04 100644
--- a/lib/routes/iguoguo/namespace.ts
+++ b/lib/routes/iguoguo/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '爱果果',
url: 'iguoguo.net',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/iheima/index.ts b/lib/routes/iheima/index.ts
new file mode 100644
index 00000000000000..1f105587d5b2ca
--- /dev/null
+++ b/lib/routes/iheima/index.ts
@@ -0,0 +1,44 @@
+import { Route } from '@/types';
+import got from '@/utils/got';
+import { parseDate } from '@/utils/parse-date';
+
+export const route: Route = {
+ path: '/recommend',
+ categories: ['new-media'],
+ example: '/iheima/recommend',
+ url: 'www.iheima.com',
+ name: '推荐',
+ maintainers: ['p3psi-boo'],
+ handler,
+};
+
+async function handler() {
+ const baseUrl = 'https://www.iheima.com/?page=1&pagesize=20';
+
+ const response = await got({
+ method: 'get',
+ url: baseUrl,
+ responseType: 'json',
+ headers: {
+ Accept: 'application/json, text/javascript, */*; q=0.01',
+ Referer: 'https://www.iheima.com/',
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ });
+
+ const content = JSON.parse(response.body);
+ const list = content.contents;
+
+ const items = list.map((item) => ({
+ title: item.title,
+ link: item.url,
+ pubDate: parseDate(item.published),
+ description: item.content,
+ }));
+
+ return {
+ title: '推荐',
+ link: baseUrl,
+ item: items,
+ };
+}
diff --git a/lib/routes/iheima/namespace.ts b/lib/routes/iheima/namespace.ts
new file mode 100644
index 00000000000000..b276ca53c5eaf5
--- /dev/null
+++ b/lib/routes/iheima/namespace.ts
@@ -0,0 +1,8 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'i黑马网',
+ url: 'www.iheima.com',
+ description: '',
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/iiilab/namespace.ts b/lib/routes/iiilab/namespace.ts
index bb0a144557f727..9b0ce9cbcb84b9 100644
--- a/lib/routes/iiilab/namespace.ts
+++ b/lib/routes/iiilab/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '人人都是自媒体',
url: 'www.iiilab.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/ikea/cn/low-price.ts b/lib/routes/ikea/cn/low-price.ts
index d1c5d1a56d71df..994466f348258a 100644
--- a/lib/routes/ikea/cn/low-price.ts
+++ b/lib/routes/ikea/cn/low-price.ts
@@ -32,7 +32,7 @@ async function handler() {
headers: generateRequestHeaders(),
searchParams: {
processOutOfStock: 'SORT',
- groupId: 'cms_低价好物_cms-商品列表-_0',
+ groupId: 'cms_product_cn--zh--8b08af400ac511ec909ec36c6e99b004_0_0',
page: 1,
size: 200,
},
@@ -42,6 +42,7 @@ async function handler() {
title: 'IKEA 宜家 - 低价优选',
link: 'https://www.ikea.cn/cn/zh/campaigns/wo3-men2-de-chao1-zhi2-di1-jia4-pub8b08af40',
description: '低价优选',
+ allowEmpty: true,
item: response.data.products.map((element) => generateProductItem(element)),
};
}
diff --git a/lib/routes/ikea/namespace.ts b/lib/routes/ikea/namespace.ts
index 892618439f2582..c74cff7000c770 100644
--- a/lib/routes/ikea/namespace.ts
+++ b/lib/routes/ikea/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'IKEA',
url: 'ikea.com',
+ lang: 'en',
};
diff --git a/lib/routes/iknowwhatyoudownload/daily.ts b/lib/routes/iknowwhatyoudownload/daily.ts
new file mode 100644
index 00000000000000..953c64502d35a3
--- /dev/null
+++ b/lib/routes/iknowwhatyoudownload/daily.ts
@@ -0,0 +1,112 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import { load } from 'cheerio';
+import dayjs from 'dayjs';
+import got from '@/utils/got';
+import { art } from '@/utils/render';
+import path from 'path';
+import { getCurrentPath } from '@/utils/helpers';
+
+const __dirname = getCurrentPath(import.meta.url);
+
+interface TableData {
+ key: string;
+ count: string;
+ percent: string;
+}
+
+export const route: Route = {
+ path: '/stats/daily/:country',
+ categories: ['other'],
+ example: '/iknowwhatyoudownload/stats/daily/CN',
+ url: 'iknowwhatyoudownload.com',
+ name: 'Daily Torrents Statistics',
+ maintainers: ['p3psi-boo'],
+ parameters: { country: 'the country of the stats. ISO 3166-1 alpha-2 code.' },
+ handler,
+};
+
+async function handler(ctx) {
+ const { country } = ctx.req.param();
+ const baseUrl = `https://iknowwhatyoudownload.com/en/stat/${country}/daily/q?statDate=`;
+
+ const dates = Array.from({ length: 7 }, (_, i) => dayjs().subtract(i, 'day'));
+
+ const items = (
+ await Promise.all(
+ dates.map((dateObj) => {
+ const dateFormatted = dateObj.format('YYYY-MM-DD');
+ const url = `${baseUrl}${dateFormatted}`;
+ return cache.tryGet(url, async () => {
+ const response = await got({
+ method: 'get',
+ url,
+ });
+
+ if (!response) {
+ return {};
+ }
+
+ const $ = load(response.data);
+
+ const numStats: { percent: string; desc: string }[] = [];
+ $('.usePercent').each((_, elem) => {
+ numStats.push({
+ percent: $(elem).text(),
+ desc: $(elem).parent().find('span').last().text(),
+ });
+ });
+
+ const tableData: TableData[] = [];
+ const dataMatch = response.data.match(/data:\s*\[([\d",\s]+)\]/);
+ const labelsMatch = response.data.match(/labels:\s*\[(.*?)\]/);
+
+ if (dataMatch?.[1] && labelsMatch?.[1]) {
+ const dataList = dataMatch[1].split(',').map((s) => s.trim().replaceAll('"', ''));
+ const labelsList = labelsMatch[1]
+ .split(',')
+ .map((s) => s.replaceAll('"', '').trim())
+ .filter((i) => i !== '');
+
+ for (const index in labelsList) {
+ const label = labelsList[index];
+ const count = dataList[index];
+ const [key, percent] = label.split(' ');
+ tableData.push({
+ key,
+ count,
+ percent,
+ });
+ }
+ }
+
+ const topList = $('.tab-pane')
+ .toArray()
+ .map((item) => ({
+ title: $(item).attr('id')?.toUpperCase(),
+ content: $(item).find('ul').toString(),
+ }));
+
+ const content = art(path.join(__dirname, 'templates/daily.art'), {
+ numStats,
+ tableData,
+ topList,
+ });
+
+ return {
+ title: `Daily Torrents Statistics in ${country} for ${dateFormatted}`,
+ link: url,
+ description: content,
+ pubDate: dateObj.toDate(),
+ };
+ });
+ })
+ )
+ ).filter((item) => Object.keys(item).length > 0);
+
+ return {
+ title: `Daily Torrents Statistics in ${country} - iknownwhatyoudownload`,
+ link: 'https://iknowwhatyoudownload.com',
+ item: items,
+ };
+}
diff --git a/lib/routes/iknowwhatyoudownload/namespace.ts b/lib/routes/iknowwhatyoudownload/namespace.ts
new file mode 100644
index 00000000000000..2074a1397e53cc
--- /dev/null
+++ b/lib/routes/iknowwhatyoudownload/namespace.ts
@@ -0,0 +1,8 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'I Know What You Download',
+ url: 'iknowwhatyoudownload.com',
+ description: '',
+ lang: 'en',
+};
diff --git a/lib/routes/iknowwhatyoudownload/templates/daily.art b/lib/routes/iknowwhatyoudownload/templates/daily.art
new file mode 100644
index 00000000000000..1466aea09c3374
--- /dev/null
+++ b/lib/routes/iknowwhatyoudownload/templates/daily.art
@@ -0,0 +1,34 @@
+Torrent download statistics
+
+ {{each numStats}}
+
+ Table View
+ {{if tableData}}
+
+
+ {{/if}}
+
+ {{each tableData}}
+ Category Count Percent
+
+ {{/each}}
+ {{$value.key}}
+ {{$value.count}}
+ {{$value.percent}}
+ Top List
+ {{each topList}}
+ {{$value.title}}
+ {{@ $value.content}}
+ {{/each}}
+
+
+{{ /if }}
+
+Original title: {{ originalTitleText.text }}
+
+{{ if certificate }}{{ certificate.rating }}{{ /if }}
+{{ if ratingsSummary.aggregateRating }}IMDb RATING: {{ ratingsSummary.aggregateRating }}/10 ({{ ratingsSummary.voteCount }}){{ /if }}
+
+
+{{ plot.plotText.plainText }}
diff --git a/lib/routes/imdb/types.ts b/lib/routes/imdb/types.ts
new file mode 100644
index 00000000000000..06ef24f3fc9484
--- /dev/null
+++ b/lib/routes/imdb/types.ts
@@ -0,0 +1,103 @@
+interface SearchFacet {
+ filterId: string;
+ text: string;
+ total: number;
+ __typename: string;
+}
+
+interface TitleGenres {
+ genre: {
+ text: string;
+ __typename: string;
+ };
+ __typename: string;
+}
+
+interface Title {
+ id: string;
+ titleText: {
+ text: string;
+ __typename: string;
+ };
+ titleType: {
+ id: string;
+ text: string;
+ canHaveEpisodes: boolean;
+ displayableProperty: {
+ value: {
+ plainText: string;
+ __typename: string;
+ };
+ __typename: string;
+ };
+ __typename: string;
+ };
+ originalTitleText: {
+ text: string;
+ __typename: string;
+ };
+ primaryImage: {
+ id: string;
+ width: number;
+ height: number;
+ url: string;
+ caption: {
+ plainText: string;
+ __typename: string;
+ };
+ __typename: string;
+ };
+ releaseYear: {
+ year: number;
+ endYear: number | null;
+ __typename: string;
+ };
+ ratingsSummary: {
+ aggregateRating: number;
+ voteCount: number;
+ __typename: string;
+ };
+ runtime: {
+ seconds: number;
+ __typename: string;
+ };
+ certificate: {
+ rating: string;
+ __typename: string;
+ } | null;
+ canRate: {
+ isRatable: boolean;
+ __typename: string;
+ };
+ titleGenres: {
+ genres: TitleGenres[];
+ __typename: string;
+ };
+ canHaveEpisodes: boolean;
+ plot: {
+ plotText: {
+ plainText: string;
+ __typename: string;
+ };
+ __typename: string;
+ };
+ latestTrailer: {
+ id: string;
+ __typename: string;
+ } | null;
+ series: null;
+ __typename: string;
+}
+
+interface ChartTitleEdge {
+ currentRank: number;
+ node: Title;
+ __typename: string;
+}
+
+export interface ChartTitleSearchConnection {
+ edges: ChartTitleEdge[];
+ genres: SearchFacet[];
+ keywords: SearchFacet[];
+ __typename: string;
+}
diff --git a/lib/routes/imhcg/blog.ts b/lib/routes/imhcg/blog.ts
new file mode 100644
index 00000000000000..c365db55d0f208
--- /dev/null
+++ b/lib/routes/imhcg/blog.ts
@@ -0,0 +1,47 @@
+import { Route, ViewType } from '@/types';
+import ofetch from '@/utils/ofetch';
+import { load } from 'cheerio';
+
+export const route: Route = {
+ path: '/',
+ categories: ['blog'],
+ view: ViewType.Notifications,
+ example: '/imhcg',
+ parameters: {},
+ radar: [
+ {
+ source: ['infos.imhcg.cn'],
+ },
+ ],
+ name: 'Engineering blogs',
+ maintainers: ['ZiHao256'],
+ handler,
+ url: 'infos.imhcg.cn',
+};
+
+async function handler() {
+ const response = await ofetch('https://infos.imhcg.cn/');
+ const $ = load(response);
+ const items = $('li')
+ .toArray()
+ .map((item) => {
+ const title = $(item).find('a.title').text();
+ const link = $(item).find('a.title').attr('href');
+ const author = $(item).find('p.author').text();
+ const time = $(item).find('p.time').text();
+ const description = $(item).find('p.text').text();
+ return {
+ title,
+ link,
+ author,
+ time,
+ description,
+ };
+ });
+
+ return {
+ title: `Engineering Blogs`,
+ link: 'https://infos.imhcg.cn/',
+ item: items,
+ };
+}
diff --git a/lib/routes/imhcg/namespace.ts b/lib/routes/imhcg/namespace.ts
new file mode 100644
index 00000000000000..d014fc2877126b
--- /dev/null
+++ b/lib/routes/imhcg/namespace.ts
@@ -0,0 +1,8 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'imhcg的信息站',
+ url: 'infos.imhcg.cn',
+ description: '包含多种技术和新闻信息的网站',
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/imiker/namespace.ts b/lib/routes/imiker/namespace.ts
index 9a06cdda57de1c..eeef5eb60edfcb 100644
--- a/lib/routes/imiker/namespace.ts
+++ b/lib/routes/imiker/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '米课',
url: 'imiker.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/imop/namespace.ts b/lib/routes/imop/namespace.ts
new file mode 100644
index 00000000000000..6d47e41d8c9fb7
--- /dev/null
+++ b/lib/routes/imop/namespace.ts
@@ -0,0 +1,11 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'imop',
+ url: 'imop.com',
+
+ zh: {
+ name: '千橡游戏',
+ },
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/imop/tianshu.ts b/lib/routes/imop/tianshu.ts
new file mode 100644
index 00000000000000..62b66f7a35a7b6
--- /dev/null
+++ b/lib/routes/imop/tianshu.ts
@@ -0,0 +1,53 @@
+import { Route } from '@/types';
+import { load } from 'cheerio';
+import got from '@/utils/got';
+import iconv from 'iconv-lite';
+import cache from '@/utils/cache';
+
+const baseUrl = 'http://t.imop.com';
+
+export const route: Route = {
+ path: '/tianshu',
+ categories: ['game'],
+ example: '/imop/tianshu',
+ radar: [
+ {
+ source: ['t.imop.com'],
+ target: '/tianshu',
+ },
+ ],
+ name: '全部消息',
+ maintainers: ['zhkgo'],
+ handler,
+};
+
+async function handler() {
+ const { data: response } = await got(`${baseUrl}/list/0-1.htm`, { responseType: 'buffer' });
+ const $ = load(iconv.decode(response, 'gbk'));
+ const list = $('.right .right_top .right_bot .list2 .ul1 ul')
+ .toArray()
+ .map((item) => {
+ item = $(item);
+ const href: string = item.find('a').attr('href');
+ return {
+ title: item.find('a').text(),
+ link: href.startsWith('http') ? href : `${baseUrl}${href}`,
+ };
+ });
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const { data: response } = await got(item.link, { responseType: 'buffer' });
+ const $ = load(iconv.decode(response, 'gbk'));
+ item.description = $('.right .right_top .right_bot .articlebox').html();
+ return item;
+ })
+ )
+ );
+
+ return {
+ title: '天书最新消息',
+ link: `${baseUrl}/list/0-1.htm`,
+ item: items,
+ };
+}
diff --git a/lib/routes/indiansinkuwait/namespace.ts b/lib/routes/indiansinkuwait/namespace.ts
index 3a6ff536edd5e4..d8a69da61a9450 100644
--- a/lib/routes/indiansinkuwait/namespace.ts
+++ b/lib/routes/indiansinkuwait/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Indians in Kuwait',
url: 'indiansinkuwait.com',
+ lang: 'en',
};
diff --git a/lib/routes/indienova/column.ts b/lib/routes/indienova/column.ts
index a0db2355c6059b..e0f3e1ecee1735 100644
--- a/lib/routes/indienova/column.ts
+++ b/lib/routes/indienova/column.ts
@@ -26,44 +26,44 @@ export const route: Route = {
maintainers: ['TonyRL'],
handler,
description: `专题 ID
+专题 ID
游戏推荐
- | itch 一周游戏汇 | 一周值得关注的发售作品 | 诺娃速递 | 周末游戏视频集锦 | 每月媒体评分 | 年度最佳游戏 | Indie Focus 近期新游 | indienova Picks 精选 |
- | --------------- | ---------------------- | -------- | ---------------- | ------------ | ------------ | -------------------- | -------------------- |
- | 52 | 29 | 41 | 43 | 45 | 39 | 1 | 8 |
+| itch 一周游戏汇 | 一周值得关注的发售作品 | 诺娃速递 | 周末游戏视频集锦 | 每月媒体评分 | 年度最佳游戏 | Indie Focus 近期新游 | indienova Picks 精选 |
+| --------------- | ---------------------- | -------- | ---------------- | ------------ | ------------ | -------------------- | -------------------- |
+| 52 | 29 | 41 | 43 | 45 | 39 | 1 | 8 |
游戏评论
- | 游必有方 Podcast | 独立游戏潮(RED) |
- | ---------------- | ----------------- |
- | 6 | 3 |
+| 游必有方 Podcast | 独立游戏潮(RED) |
+| ---------------- | ----------------- |
+| 6 | 3 |
游戏开发
- | 游戏设计模式 | Roguelike 开发 | GMS 中文教程 |
- | ------------ | -------------- | ------------ |
- | 15 | 14 | 7 |
+| 游戏设计模式 | Roguelike 开发 | GMS 中文教程 |
+| ------------ | -------------- | ------------ |
+| 15 | 14 | 7 |
游戏设计
- | 游戏与所有 | 让人眼前一亮的游戏设计 | 游戏音乐分析 | 游戏情感设计 | 游戏相关书籍 | 游戏设计课程笔记 | 游戏设计工具 | 游戏设计灵感 | 设计师谈设计 | 游戏研究方法 | 功能游戏 | 游戏设计专业院校 | 像素课堂 |
- | ---------- | ---------------------- | ------------ | ------------ | ------------ | ---------------- | ------------ | ------------ | ------------ | ------------ | -------- | ---------------- | -------- |
- | 10 | 33 | 17 | 4 | 22 | 11 | 24 | 26 | 27 | 28 | 38 | 9 | 19 |
+| 游戏与所有 | 让人眼前一亮的游戏设计 | 游戏音乐分析 | 游戏情感设计 | 游戏相关书籍 | 游戏设计课程笔记 | 游戏设计工具 | 游戏设计灵感 | 设计师谈设计 | 游戏研究方法 | 功能游戏 | 游戏设计专业院校 | 像素课堂 |
+| ---------- | ---------------------- | ------------ | ------------ | ------------ | ---------------- | ------------ | ------------ | ------------ | ------------ | -------- | ---------------- | -------- |
+| 10 | 33 | 17 | 4 | 22 | 11 | 24 | 26 | 27 | 28 | 38 | 9 | 19 |
游戏文化
- | NOVA 海外独立游戏见闻 | 工作室访谈 | indie Figure 游戏人 | 游戏艺术家 | 独立游戏音乐欣赏 | 游戏瑰宝 | 电脑 RPG 游戏史 | ALT. CTRL. GAMING |
- | --------------------- | ---------- | ------------------- | ---------- | ---------------- | -------- | --------------- | ----------------- |
- | 53 | 23 | 5 | 44 | 18 | 21 | 16 | 2 |
+| NOVA 海外独立游戏见闻 | 工作室访谈 | indie Figure 游戏人 | 游戏艺术家 | 独立游戏音乐欣赏 | 游戏瑰宝 | 电脑 RPG 游戏史 | ALT. CTRL. GAMING |
+| --------------------- | ---------- | ------------------- | ---------- | ---------------- | -------- | --------------- | ----------------- |
+| 53 | 23 | 5 | 44 | 18 | 21 | 16 | 2 |
Game Jam
- | Ludum Dare | Global Game Jam |
- | ---------- | --------------- |
- | 31 | 13 |
-
+
{{ intro }}
+{{ /if }}
+
+{{ if description }}
+ {{@ description }}
+{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/infoq/topic.ts b/lib/routes/infoq/topic.ts
index 7915b18bddfb0d..77d41671963171 100644
--- a/lib/routes/infoq/topic.ts
+++ b/lib/routes/infoq/topic.ts
@@ -5,7 +5,7 @@ import utils from './utils';
export const route: Route = {
path: '/topic/:id',
- categories: ['new-media'],
+ categories: ['new-media', 'popular'],
example: '/infoq/topic/1',
parameters: { id: '话题id,可在 [InfoQ全部话题](https://www.infoq.cn/topics) 页面找到URL里的话题id' },
features: {
diff --git a/lib/routes/informedainews/docs.ts b/lib/routes/informedainews/docs.ts
new file mode 100644
index 00000000000000..12ac700693803b
--- /dev/null
+++ b/lib/routes/informedainews/docs.ts
@@ -0,0 +1,78 @@
+import { Route } from '@/types';
+import ofetch from '@/utils/ofetch'; // 统一使用的请求库
+import { load } from 'cheerio'; // 类似 jQuery 的 API HTML 解析器
+import { parseDate } from '@/utils/parse-date';
+import cache from '@/utils/cache';
+
+export const route: Route = {
+ path: '/zh-Hans/docs/:type',
+ categories: ['new-media', 'popular'],
+ example: '/informedainews/zh-Hans/docs/world-news-daily',
+ parameters: { type: 'world-news-daily|tech-enthusiast-weekly|ai-enthusiast-daily' },
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['informedainews.com', 'informedainews.com/zh-Hans/docs/:type', 'informedainews.com/docs/:type'],
+ target: '/zh-Hans/docs/:type',
+ },
+ ],
+ name: '知闻AI',
+ maintainers: ['guicaiyue'],
+ handler,
+};
+
+async function handler(ctx) {
+ const { type } = ctx.req.param();
+ const response = await ofetch(`https://informedainews.com/zh-Hans/docs/${type}`);
+ const $ = load(response);
+ const list = $('li.theme-doc-sidebar-item-category ul li')
+ .toArray()
+ .map((item) => {
+ item = $(item);
+ const a = item.find('a').first();
+ const text = a.text();
+ // 找到第一个'('字符的位置
+ const start = text.indexOf('(');
+ // 找到第一个')'字符的位置
+ const end = text.indexOf(')');
+ // 从第一个'('到第一个')'之间的子字符串就是日期
+ const date = text.substring(start + 1, end);
+ return {
+ title: text,
+ link: `https://informedainews.com${a.attr('href')}`,
+ pubDate: parseDate(date),
+ author: 'AI',
+ };
+ });
+
+ const items = await Promise.all(
+ list.map((item) =>
+ cache.tryGet(item.link, async () => {
+ const response = await ofetch(item.link);
+ const $ = load(response);
+
+ // 选择类名为“comment-body”的第一个元素
+ item.description = $('.theme-doc-markdown.markdown').first().html();
+
+ // 上面每个列表项的每个属性都在此重用,
+ // 并增加了一个新属性“description”
+ return item;
+ })
+ )
+ );
+ return {
+ // 源标题
+ title: `${type} docs`,
+ // 源链接
+ link: `https://informedainews.com/zh-Hans/docs/${type}`,
+ // 源文章
+ item: items,
+ };
+}
diff --git a/lib/routes/informedainews/namespace.ts b/lib/routes/informedainews/namespace.ts
new file mode 100644
index 00000000000000..7526998b1aff7e
--- /dev/null
+++ b/lib/routes/informedainews/namespace.ts
@@ -0,0 +1,19 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Informed AI News',
+ url: 'informedainews.com',
+ description: `
+::: tip
+informed AI RSS feeds:
+
+- World News Daily: 'https://rsshub.app/informedainews/zh-Hans/docs/world-news-daily'
+- Tech Enthusiast Weekly: 'https://rsshub.app/informedainews/zh-Hans/docs/tech-enthusiast-weekly'
+- AI Enthusiast Weekly: 'https://rsshub.app/informedainews/zh-Hans/docs/ai-enthusiast-daily'
+:::`,
+
+ zh: {
+ name: '知闻AI',
+ },
+ lang: 'en',
+};
diff --git a/lib/routes/informs/namespace.ts b/lib/routes/informs/namespace.ts
index 36ee623edb0fec..ed1d87345471ca 100644
--- a/lib/routes/informs/namespace.ts
+++ b/lib/routes/informs/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'INFORMS',
url: 'pubsonline.informs.org',
+ lang: 'en',
};
diff --git a/lib/routes/infzm/hot.ts b/lib/routes/infzm/hot.ts
new file mode 100644
index 00000000000000..5801f9c6e0a52e
--- /dev/null
+++ b/lib/routes/infzm/hot.ts
@@ -0,0 +1,39 @@
+import type { Data, DataItem, Route } from '@/types';
+import type { ContentsResponse } from './types';
+import got from '@/utils/got';
+import { fetchArticles } from './utils';
+
+export const route: Route = {
+ path: '/hot',
+ parameters: {},
+ categories: ['traditional-media'],
+ example: '/infzm/hot',
+ radar: [
+ {
+ source: ['infzm.com/'],
+ },
+ ],
+ name: '热门文章',
+ maintainers: ['KarasuShin', 'ranpox', 'xyqfer'],
+ handler,
+};
+
+async function handler(): Promise {
+ const link = 'https://www.infzm.com/';
+ const { data } = await got
'),
+ pubDate: parseDate(item.created),
+ updated: parseDate(item.updated),
+ category: item.metadata.keywords?.map((k) => k.value),
+ author: item.metadata.authors.map((a) => `${a.first_name} ${a.last_name}${a.affiliations ? ` (${a.affiliations.map((aff) => aff.value).join(', ')})` : ''}`).join(', '),
+ }));
diff --git a/lib/routes/instagram/namespace.ts b/lib/routes/instagram/namespace.ts
index fc574ff8e113b2..a3f19d9c7520d0 100644
--- a/lib/routes/instagram/namespace.ts
+++ b/lib/routes/instagram/namespace.ts
@@ -3,7 +3,8 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Instagram',
url: 'www.instagram.com',
- description: `:::tip
+ description: `::: tip
It's highly recommended to deploy with Redis cache enabled.
:::`,
+ lang: 'en',
};
diff --git a/lib/routes/instagram/private-api/index.ts b/lib/routes/instagram/private-api/index.ts
index b42a8beda7752c..bd4bd539dcd967 100644
--- a/lib/routes/instagram/private-api/index.ts
+++ b/lib/routes/instagram/private-api/index.ts
@@ -1,4 +1,4 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import cache from '@/utils/cache';
import { ig, login } from './utils';
import logger from '@/utils/logger';
@@ -59,8 +59,25 @@ async function loadContent(category, nameOrId, tryGet) {
export const route: Route = {
path: '/:category/:key',
categories: ['social-media'],
+ view: ViewType.SocialMedia,
example: '/instagram/user/stefaniejoosten',
- parameters: { category: 'Feed category, see table above', key: 'Username / Hashtag name' },
+ parameters: {
+ category: {
+ description: 'Feed category',
+ default: 'user',
+ options: [
+ {
+ label: 'User',
+ value: 'user',
+ },
+ {
+ label: 'Tags',
+ value: 'tags',
+ },
+ ],
+ },
+ key: 'Username / Hashtag name',
+ },
features: {
requireConfig: [
{
@@ -68,6 +85,14 @@ export const route: Route = {
optional: true,
description: '',
},
+ {
+ name: 'IG_USERNAME',
+ description: 'Instagram username',
+ },
+ {
+ name: 'IG_PASSWORD',
+ description: 'Instagram password, due to [Instagram Private API](https://github.com/dilame/instagram-private-api) restrictions, you have to setup your credentials on the server. 2FA is not supported.',
+ },
],
requirePuppeteer: false,
antiCrawler: true,
@@ -78,9 +103,6 @@ export const route: Route = {
name: 'User Profile / Hashtag - Private API',
maintainers: ['oppilate', 'DIYgod'],
handler,
- description: `:::warning
-Due to [Instagram Private API](https://github.com/dilame/instagram-private-api) restrictions, you have to setup your credentials on the server. 2FA is not supported. See [deployment guide](https://docs.rsshub.app/deploy/) for more.
-:::`,
};
async function handler(ctx) {
diff --git a/lib/routes/instagram/web-api/index.ts b/lib/routes/instagram/web-api/index.ts
index 6acc1249b4cc38..f390861868bdef 100644
--- a/lib/routes/instagram/web-api/index.ts
+++ b/lib/routes/instagram/web-api/index.ts
@@ -23,7 +23,7 @@ export const route: Route = {
name: 'User Profile / Hashtag',
maintainers: ['TonyRL'],
handler,
- description: `:::tip
+ description: `::: tip
You may need to setup cookie for a less restrictive rate limit and private profiles.
:::
diff --git a/lib/routes/instructables/namespace.ts b/lib/routes/instructables/namespace.ts
index 09df9711488ccd..5985eef80988be 100644
--- a/lib/routes/instructables/namespace.ts
+++ b/lib/routes/instructables/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Instructables',
url: 'instructables.com',
+ lang: 'en',
};
diff --git a/lib/routes/instructables/projects.ts b/lib/routes/instructables/projects.ts
index ec1a5af0f863d6..5708fd6ed818b3 100644
--- a/lib/routes/instructables/projects.ts
+++ b/lib/routes/instructables/projects.ts
@@ -1,5 +1,7 @@
import { Route } from '@/types';
-import got from '@/utils/got';
+import ofetch from '@/utils/ofetch';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/projects/:category?',
@@ -25,15 +27,15 @@ export const route: Route = {
handler,
url: 'instructables.com/projects',
description: `| All | Circuits | Workshop | Craft | Cooking | Living | Outside | Teachers |
- | --- | -------- | -------- | ----- | ------- | ------ | ------- | -------- |
- | | circuits | workshop | craft | cooking | living | outside | teachers |`,
+| --- | -------- | -------- | ----- | ------- | ------ | ------- | -------- |
+| | circuits | workshop | craft | cooking | living | outside | teachers |`,
};
async function handler(ctx) {
- const category = ctx.req.param('category') ?? 'all';
+ const { category = 'all' } = ctx.req.param();
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 50;
- const siteDomain = 'www.instructables.com';
- const apiKey = 'NU5CdGwyRDdMVnVmM3l4cWNqQzFSVzJNZU5jaUxFU3dGK3J2L203MkVmVT02ZWFYeyJleGNsdWRlX2ZpZWxkcyI6WyJvdXRfb2YiLCJzZWFyY2hfdGltZV9tcyIsInN0ZXBCb2R5Il0sInBlcl9wYWdlIjo1MH0=';
+ const siteDomain = 'instructables.com';
let pathPrefix, projectFilter;
if (category === 'all') {
@@ -45,32 +47,35 @@ async function handler(ctx) {
projectFilter = category === 'teachers' ? `&& teachers:=${filterValue}` : ` && category:=${filterValue}`;
}
- const link = `https://${siteDomain}/${pathPrefix}projects?projects=all`;
+ const pageLink = `https://${siteDomain}/${pathPrefix}projects`;
- const response = await got({
+ const pageResponse = await ofetch(pageLink);
+ const $ = load(pageResponse);
+ const { typesenseProxy, typesenseApiKey } = JSON.parse($('script#js-page-context').text());
+
+ const data = await ofetch(`${typesenseProxy}/collections/projects/documents/search`, {
method: 'get',
- url: `https://${siteDomain}/api_proxy/search/collections/projects/documents/search`,
+ baseURL: `https://${siteDomain}`,
headers: {
- Referer: link,
+ Referer: pageLink,
Host: siteDomain,
- 'x-typesense-api-key': apiKey,
+ 'x-typesense-api-key': typesenseApiKey,
},
- searchParams: {
+ query: {
q: '*',
query_by: 'title,stepBody,screenName',
page: 1,
- per_page: 50,
+ per_page: limit,
sort_by: 'publishDate:desc',
include_fields: 'title,urlString,coverImageUrl,screenName,publishDate,favorites,views,primaryClassification,featureFlag,prizeLevel,IMadeItCount',
filter_by: `featureFlag:=true${projectFilter}`,
},
+ parseResponse: JSON.parse,
});
- const data = response.data;
-
return {
title: 'Instructables Projects', // 项目的标题
- link, // 指向项目的链接
+ link: `https://${siteDomain}/projects`, // 指向项目的链接
description: 'Instructables Projects', // 描述项目
language: 'en', // 频道语言
item: data.hits.map((item) => ({
@@ -78,7 +83,7 @@ async function handler(ctx) {
link: `https://${siteDomain}/${item.document.urlString}`,
author: item.document.screenName,
description: ``,
- pubDate: new Date(item.document.publishDate).toUTCString(),
+ pubDate: parseDate(item.document.publishDate),
category: item.document.primaryClassification,
})),
};
diff --git a/lib/routes/investor/index.ts b/lib/routes/investor/index.ts
new file mode 100644
index 00000000000000..b4f733b011b778
--- /dev/null
+++ b/lib/routes/investor/index.ts
@@ -0,0 +1,213 @@
+import { Route } from '@/types';
+
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { parseDate } from '@/utils/parse-date';
+
+// Collected from https://www.investor.org.cn/images/docSearchData.js.
+
+const channelIds = {
+ 63: 298519,
+ 958: 244863,
+ 3966: 244863,
+};
+
+export const handler = async (ctx) => {
+ const { category = 'information_release/news_release_from_authorities/zjhfb' } = ctx.req.param();
+ const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15;
+
+ const rootUrl = 'https://www.investor.org.cn';
+ const apiUrl = new URL('was5/web/search', rootUrl).href;
+ const currentUrl = new URL(category.endsWith('/') ? category : `${category}/`, rootUrl).href;
+
+ const { data: response } = await got(currentUrl);
+
+ const $ = load(response);
+
+ const language = 'zh';
+
+ let items = [];
+
+ if ($('div.hotlist dd').length === 0) {
+ const channelId = response.match(/params.channelId='(\d+)';/)?.[1] ?? undefined;
+
+ const {
+ data: { rows },
+ } = await got.post(apiUrl, {
+ form: {
+ channelid: channelIds?.[channelId] ?? undefined,
+ searchword: `CHANNELID=${channelId}`,
+ page: 1,
+ perpage: limit,
+ },
+ });
+
+ items = rows.map((item) => ({
+ title: item.DOCTITLE,
+ pubDate: parseDate(item.DOCPUBTIME),
+ link: item.DOCURL,
+ language,
+ }));
+ } else {
+ items = $('div.hotlist dd')
+ .slice(0, limit)
+ .toArray()
+ .map((item) => {
+ item = $(item);
+
+ const href = item.find('a').prop('href');
+
+ return {
+ title: item.find('a').prop('title'),
+ pubDate: parseDate(item.find('span.date').text()),
+ link: new URL(href, currentUrl).href,
+ language,
+ };
+ });
+ }
+
+ items = await Promise.all(
+ items.map((item) =>
+ cache.tryGet(item.link, async () => {
+ if (!item.link.endsWith('html')) {
+ return item;
+ }
+
+ const { data: detailResponse } = await got(item.link);
+
+ const $$ = load(detailResponse);
+
+ const title = $$('div.contentText h2').text();
+ const description = $$('div.TRS_Editor').html();
+
+ item.title = title || item.title;
+ item.description = description;
+ item.author = $$('span.timeSpan')
+ .text()
+ .trim()
+ .split(/来源:/)
+ .pop();
+ item.content = {
+ html: description,
+ text: $$('div.TRS_Editor').text(),
+ };
+ item.language = language;
+
+ return item;
+ })
+ )
+ );
+
+ const image = $('div.img_cursor a img').prop('src');
+
+ return {
+ title: $('title').text(),
+ description: $('meta[name="apple-mobile-web-app-title"]').prop('content'),
+ link: currentUrl,
+ item: items,
+ allowEmpty: true,
+ image,
+ author: $('meta[name="author"]').prop('content'),
+ language,
+ };
+};
+
+export const route: Route = {
+ path: '/:category{.+}?',
+ name: '分类',
+ url: 'investor.org.cn',
+ maintainers: ['nczitzk'],
+ handler,
+ example: '/investor/information_release/news_release_from_authorities/zjhfb',
+ parameters: { category: '分类,默认为证监会发布 `information_release/news_release_from_authorities/zjhfb`,可在对应分类页 URL 中找到' },
+ description: `::: tip
+ 若订阅 [证监会发布](https://www.investor.org.cn/information_release/news_release_from_authorities/zjhfb/),网址为 \`https://www.investor.org.cn/information_release/news_release_from_authorities/zjhfb/\`。截取 \`https://www.investor.org.cn/\` 到末尾 \`/\` 的部分 \`information_release/news_release_from_authorities/zjhfb\` 作为参数填入,此时路由为 [\`/investor/information_release/news_release_from_authorities/zjhfb\`](https://rsshub.app/investor/information_release/news_release_from_authorities/zjhfb)。
+:::
+
+#### [权威发布](https://www.investor.org.cn/information_release/news_release_from_authorities/)
+
+| [证监会发布](https://www.investor.org.cn/information_release/news_release_from_authorities/zjhfb/) | [证券交易所发布](https://www.investor.org.cn/information_release/news_release_from_authorities/hsjysfb/) | [期货交易所发布](https://www.investor.org.cn/information_release/news_release_from_authorities/qhjysfb/) | [行业协会发布](https://www.investor.org.cn/information_release/news_release_from_authorities/hyxhfb/) | [其他](https://www.investor.org.cn/information_release/news_release_from_authorities/otner/) |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [/investor/information_release/news_release_from_authorities/zjhfb/](https://rsshub.app/investor/investor/information_release/news_release_from_authorities/zjhfb/) | [/investor/information_release/news_release_from_authorities/hsjysfb/](https://rsshub.app/investor/investor/information_release/news_release_from_authorities/hsjysfb/) | [/investor/information_release/news_release_from_authorities/qhjysfb/](https://rsshub.app/investor/investor/information_release/news_release_from_authorities/qhjysfb/) | [/investor/information_release/news_release_from_authorities/hyxhfb/](https://rsshub.app/investor/investor/information_release/news_release_from_authorities/hyxhfb/) | [/investor/information_release/news_release_from_authorities/otner/](https://rsshub.app/investor/investor/information_release/news_release_from_authorities/otner/) |
+
+#### [市场资讯](https://www.investor.org.cn/information_release/market_news/)
+
+| [市场资讯](https://www.investor.org.cn/information_release/market_news/) |
+| ---------------------------------------------------------------------------------------------------------- |
+| [/investor/information_release/market_news/](https://rsshub.app/investor/information_release/market_news/) |
+
+#### [政策解读](https://www.investor.org.cn/information_release/policy_interpretation/)
+
+| [政策解读](https://www.investor.org.cn/information_release/policy_interpretation/) |
+| ------------------------------------------------------------------------------------------------------------------- |
+| [/investorinformation_release/policy_interpretation/](https://rsshub.appinformation_release/policy_interpretation/) |
+
+#### [国际交流](https://www.investor.org.cn/information_release/international_communication/)
+
+| [国际交流](https://www.investor.org.cn/information_release/international_communication/) |
+| --------------------------------------------------------------------------------------------------------------------------------- |
+| [/investor/information_release/international_communication/](https://rsshub.app/information_release/international_communication/) |
+ `,
+ categories: ['finance'],
+
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['investor.org.cn/:category'],
+ target: (params) => {
+ const category = params.category;
+
+ return `/investor${category ? `/${category}` : ''}`;
+ },
+ },
+ {
+ title: '权威发布 - 证监会发布',
+ source: ['www.investor.org.cn/information_release/news_release_from_authorities/zjhfb/'],
+ target: '/information_release/news_release_from_authorities/zjhfb/',
+ },
+ {
+ title: '权威发布 - 证券交易所发布',
+ source: ['www.investor.org.cn/information_release/news_release_from_authorities/hsjysfb/'],
+ target: '/information_release/news_release_from_authorities/hsjysfb/',
+ },
+ {
+ title: '权威发布 - 期货交易所发布',
+ source: ['www.investor.org.cn/information_release/news_release_from_authorities/qhjysfb/'],
+ target: '/information_release/news_release_from_authorities/qhjysfb/',
+ },
+ {
+ title: '权威发布 - 行业协会发布',
+ source: ['www.investor.org.cn/information_release/news_release_from_authorities/hyxhfb/'],
+ target: '/information_release/news_release_from_authorities/hyxhfb/',
+ },
+ {
+ title: '权威发布 - 其他',
+ source: ['www.investor.org.cn/information_release/news_release_from_authorities/otner/'],
+ target: '/information_release/news_release_from_authorities/otner/',
+ },
+ {
+ title: '市场资讯',
+ source: ['www.investor.org.cn/information_release/market_news/'],
+ target: '/information_release/market_news/',
+ },
+ {
+ title: '政策解读',
+ source: ['www.investor.org.cn/information_release/policy_interpretation/'],
+ target: '/information_release/policy_interpretation/',
+ },
+ {
+ title: '国际交流',
+ source: ['www.investor.org.cn/information_release/international_communication/'],
+ target: '/information_release/international_communication/',
+ },
+ ],
+};
diff --git a/lib/routes/investor/namespace.ts b/lib/routes/investor/namespace.ts
new file mode 100644
index 00000000000000..9e7a5a6f853966
--- /dev/null
+++ b/lib/routes/investor/namespace.ts
@@ -0,0 +1,9 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '中国投资者网',
+ url: 'investor.org.cn',
+ categories: ['finance'],
+ description: '',
+ lang: 'zh-CN',
+};
diff --git a/lib/routes/iplaysoft/category.ts b/lib/routes/iplaysoft/category.ts
new file mode 100644
index 00000000000000..2fe4fe4963c6f6
--- /dev/null
+++ b/lib/routes/iplaysoft/category.ts
@@ -0,0 +1,49 @@
+import { Data, DataItem, Route, ViewType } from '@/types';
+import { fetchNewsItems, fetchCategory } from './utils';
+
+export const handler = async (ctx): Promise => {
+ const slug = ctx.req.param('slug');
+
+ const { id, name } = await fetchCategory(slug);
+
+ const rootUrl = 'https://www.iplaysoft.com/';
+ const postApiUrl = `${rootUrl}wp-json/wp/v2/posts?_embed&categories=${id}`;
+
+ const items: DataItem[] = await fetchNewsItems(postApiUrl);
+
+ return {
+ title: `${name} - 异次元软件世界`,
+ description: '软件改变生活',
+ language: 'zh-CN',
+ link: `${rootUrl}category/${slug}`,
+ item: items,
+ };
+};
+
+export const route: Route = {
+ path: '/category/:slug',
+ name: '分类',
+ url: 'www.iplaysoft.com',
+ maintainers: ['cscnk52'],
+ handler,
+ example: '/iplaysoft/category/system',
+ parameters: { slug: '分类名称' },
+ description: undefined,
+ categories: ['program-update'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ source: ['www.iplaysoft.com/category/:slug'],
+ target: '/category/:slug',
+ },
+ ],
+ view: ViewType.Articles,
+};
diff --git a/lib/routes/iplaysoft/index.ts b/lib/routes/iplaysoft/index.ts
new file mode 100644
index 00000000000000..301569071e3ea8
--- /dev/null
+++ b/lib/routes/iplaysoft/index.ts
@@ -0,0 +1,75 @@
+import { Data, DataItem, Route, ViewType } from '@/types';
+import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch';
+import parser from '@/utils/rss-parser';
+import { parseDate } from '@/utils/parse-date';
+import { load } from 'cheerio'; // html parser
+
+export const handler = async (ctx): Promise => {
+ const feed = await parser.parseURL('https://feed.iplaysoft.com');
+ const limit = Number.parseInt(ctx.req.query('limit') || '20', 10);
+
+ const filteredItems = feed.items
+ .filter((item) => {
+ if (!item?.link || !item?.pubDate) {
+ return false;
+ }
+ return new URL(item.link).hostname.match(/.*\.iplaysoft\.com$/);
+ })
+ .slice(0, limit) as DataItem[];
+
+ const items: DataItem[] = await Promise.all(
+ filteredItems.map(
+ (item) =>
+ cache.tryGet(item.link as string, async () => {
+ const response = await ofetch(item.link);
+ const $ = load(response);
+
+ $('.entry-content').find('div[style*="overflow:hidden"]').remove();
+
+ return {
+ title: item.title,
+ description: $('.entry-content').html(),
+ link: item.link,
+ author: item.author,
+ pubDate: parseDate(item.pubDate as string),
+ } as DataItem;
+ }) as Promise
+
+
\ No newline at end of file
diff --git a/lib/routes/ipsw/namespace.ts b/lib/routes/ipsw/namespace.ts
index 359bd2215b0d32..a8b1b8e3aa11d8 100644
--- a/lib/routes/ipsw/namespace.ts
+++ b/lib/routes/ipsw/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'IPSW.me',
url: 'ipsw.me',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/iqilu/namespace.ts b/lib/routes/iqilu/namespace.ts
index babfc096531a4f..dee12f5adcedb3 100644
--- a/lib/routes/iqilu/namespace.ts
+++ b/lib/routes/iqilu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '齐鲁网',
url: 'v.iqilu.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/iqiyi/album.ts b/lib/routes/iqiyi/album.ts
index d2aed2de5be927..e2da0f4a2e4a68 100644
--- a/lib/routes/iqiyi/album.ts
+++ b/lib/routes/iqiyi/album.ts
@@ -25,9 +25,9 @@ export const route: Route = {
name: '剧集',
maintainers: ['TonyRL'],
handler,
- description: `:::tip
+ description: `::: tip
可抓取內容根据服务器所在地区而定
- :::`,
+:::`,
};
async function handler(ctx) {
diff --git a/lib/routes/iqiyi/namespace.ts b/lib/routes/iqiyi/namespace.ts
index fa26a56253bef2..760b205de2e9c6 100644
--- a/lib/routes/iqiyi/namespace.ts
+++ b/lib/routes/iqiyi/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '爱奇艺',
url: 'iq.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/iqiyi/video.ts b/lib/routes/iqiyi/video.ts
index 730281af5b54cf..37f9d62b6be43b 100644
--- a/lib/routes/iqiyi/video.ts
+++ b/lib/routes/iqiyi/video.ts
@@ -73,7 +73,7 @@ async function handler(ctx) {
config.cache.routeExpire,
false
);
- browser.close();
+ await browser.close();
return data;
}
diff --git a/lib/routes/iqnew/namespace.ts b/lib/routes/iqnew/namespace.ts
index ebd31ff0e76a86..14cc5f29232684 100644
--- a/lib/routes/iqnew/namespace.ts
+++ b/lib/routes/iqnew/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '爱 Q 生活网',
url: 'iqnew.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/iresearch/chart.ts b/lib/routes/iresearch/chart.ts
new file mode 100644
index 00000000000000..0dd82a878652d5
--- /dev/null
+++ b/lib/routes/iresearch/chart.ts
@@ -0,0 +1,375 @@
+import path from 'node:path';
+
+import { type CheerioAPI, load } from 'cheerio';
+import { type Context } from 'hono';
+
+import { type DataItem, type Route, type Data, ViewType } from '@/types';
+
+import { art } from '@/utils/render';
+import { getCurrentPath } from '@/utils/helpers';
+import ofetch from '@/utils/ofetch';
+import { parseDate } from '@/utils/parse-date';
+import timezone from '@/utils/timezone';
+
+const __dirname = getCurrentPath(import.meta.url);
+
+const categoryMap = {
+ 媒体文娱: 59,
+ 广告营销: 89,
+ 游戏行业: 90,
+ 视频媒体: 91,
+ 消费电商: 69,
+ 电子商务: 86,
+ 消费者洞察: 87,
+ 旅游行业: 88,
+ 汽车行业: 80,
+ 教育行业: 63,
+ 企业服务: 60,
+ 网络服务: 84,
+ 应用服务: 85,
+ AI大数据: 65,
+ 人工智能: 83,
+ 物流行业: 75,
+ 金融行业: 70,
+ 支付行业: 82,
+ 房产行业: 68,
+ 医疗健康: 62,
+ 先进制造: 61,
+ 能源环保: 77,
+ 区块链: 76,
+ 其他: 81,
+};
+
+export const handler = async (ctx: Context): Promise => {
+ const { category: categoryName } = ctx.req.param();
+ const limit: number = Number.parseInt(ctx.req.query('limit') ?? '100', 10);
+
+ const rootUrl: string = 'https://www.iresearch.com.cn';
+ const apiUrl = new URL('api/products/getdatasapi', rootUrl).href;
+
+ const category = categoryMap[categoryName] || undefined;
+
+ const targetUrl: string = new URL(`report.shtml?type=4${category ? `&classId=${category}` : ''}`, rootUrl).href;
+
+ const response = await ofetch(apiUrl, {
+ query: {
+ rootId: 14,
+ channelId: category ?? '',
+ userId: '',
+ lastId: '',
+ pageSize: limit,
+ },
+ });
+
+ const targetResponse = await ofetch(targetUrl);
+ const $: CheerioAPI = load(targetResponse);
+ const language: string = $('html').prop('lang') ?? 'zh-cn';
+
+ const items: DataItem[] = response.List.slice(0, limit).map((item) => ({
+ title: `${item.Title} - ${item.sTitle}`,
+ link: new URL(`chart/detail?id=${item.Id}`, rootUrl).href,
+ description: art(path.join(__dirname, 'templates/chart.art'), {
+ images: [
+ {
+ src: item.SmallImg,
+ alt: item.Title,
+ },
+ {
+ src: item.BigImg,
+ alt: item.sTitle,
+ },
+ ],
+ newsId: item.NewsId,
+ }),
+ author: item.Author,
+ category: [...new Set([item.sTitle, item.industry, ...item.Keyword])].filter(Boolean),
+ guid: `iresearch.${item.Id}`,
+ pubDate: timezone(parseDate(item.Uptime), +8),
+ }));
+
+ const author = $('title').text();
+
+ return {
+ title: `${author} | 研究图表${category ? ` - ${categoryName}` : ''}`,
+ description: $('meta[property="og:description"]').prop('content'),
+ link: targetUrl,
+ item: items,
+ allowEmpty: true,
+ author,
+ language,
+ id: targetUrl,
+ };
+};
+
+export const route: Route = {
+ path: '/chart/:category?',
+ name: '研究图表',
+ url: 'www.iresearch.com.cn',
+ maintainers: ['nczitzk'],
+ handler,
+ example: '/iresearch/chart',
+ parameters: {
+ category: '分类,见下表',
+ },
+ description: `
+| 媒体文娱 | 广告营销 | 游戏行业 | 视频媒体 | 消费电商 |
+| -------- | ---------- | -------- | --------- | -------- |
+| 电子商务 | 消费者洞察 | 旅游行业 | 汽车行业 | 教育行业 |
+| 企业服务 | 网络服务 | 应用服务 | AI 大数据 | 人工智能 |
+| 物流行业 | 金融行业 | 支付行业 | 房产行业 | 医疗健康 |
+| 先进制造 | 新能源 | 区块链 | 其他 | |
+`,
+ categories: ['new-media'],
+ features: {
+ requireConfig: false,
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportRadar: true,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ radar: [
+ {
+ title: '研究图表 - 媒体文娱',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/媒体文娱' : '';
+ },
+ },
+ {
+ title: '研究图表 - 广告营销',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/广告营销' : '';
+ },
+ },
+ {
+ title: '研究图表 - 游戏行业',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/游戏行业' : '';
+ },
+ },
+ {
+ title: '研究图表 - 视频媒体',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/视频媒体' : '';
+ },
+ },
+ {
+ title: '研究图表 - 消费电商',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/消费电商' : '';
+ },
+ },
+ {
+ title: '研究图表 - 电子商务',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/电子商务' : '';
+ },
+ },
+ {
+ title: '研究图表 - 消费者洞察',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/消费者洞察' : '';
+ },
+ },
+ {
+ title: '研究图表 - 旅游行业',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/旅游行业' : '';
+ },
+ },
+ {
+ title: '研究图表 - 汽车行业',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/汽车行业' : '';
+ },
+ },
+ {
+ title: '研究图表 - 教育行业',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/教育行业' : '';
+ },
+ },
+ {
+ title: '研究图表 - 企业服务',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/企业服务' : '';
+ },
+ },
+ {
+ title: '研究图表 - 网络服务',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/网络服务' : '';
+ },
+ },
+ {
+ title: '研究图表 - 应用服务',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/应用服务' : '';
+ },
+ },
+ {
+ title: '研究图表 - AI大数据',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/AI大数据' : '';
+ },
+ },
+ {
+ title: '研究图表 - 人工智能',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/人工智能' : '';
+ },
+ },
+ {
+ title: '研究图表 - 物流行业',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/物流行业' : '';
+ },
+ },
+ {
+ title: '研究图表 - 金融行业',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/金融行业' : '';
+ },
+ },
+ {
+ title: '研究图表 - 支付行业',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/支付行业' : '';
+ },
+ },
+ {
+ title: '研究图表 - 房产行业',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/房产行业' : '';
+ },
+ },
+ {
+ title: '研究图表 - 医疗健康',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/医疗健康' : '';
+ },
+ },
+ {
+ title: '研究图表 - 先进制造',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/先进制造' : '';
+ },
+ },
+ {
+ title: '研究图表 - 新能源',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/新能源' : '';
+ },
+ },
+ {
+ title: '研究图表 - 区块链',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/区块链' : '';
+ },
+ },
+ {
+ title: '研究图表 - 其他',
+ source: ['https://www.iresearch.com.cn/report.shtml'],
+ target: (_, url) => {
+ const urlObj = new URL(url);
+ const isChart = urlObj.searchParams.get('type') === '4';
+
+ return isChart ? '/iresearch/chart/其他' : '';
+ },
+ },
+ ],
+ view: ViewType.Articles,
+};
diff --git a/lib/routes/iresearch/namespace.ts b/lib/routes/iresearch/namespace.ts
index ed04f30c5b2034..a0fff31f3b333a 100644
--- a/lib/routes/iresearch/namespace.ts
+++ b/lib/routes/iresearch/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '艾瑞',
url: 'www.iresearch.com.cn',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/iresearch/templates/chart.art b/lib/routes/iresearch/templates/chart.art
new file mode 100644
index 00000000000000..fcff8356dfc6e0
--- /dev/null
+++ b/lib/routes/iresearch/templates/chart.art
@@ -0,0 +1,13 @@
+{{ if newsId }}
+ 查看报告
+{{ /if }}
+
+{{ if images }}
+ {{ each images image }}
+ {{ if image?.src }}
+
+
+ Version
+ {{ version }}
+
+
+ Build
+ {{ build }}
+
+
+ Released
+ {{ released }}
+
+
+
+Size
+ {{ size }}
+
+
+{{ /if }}
diff --git a/lib/routes/javtiful/utils.ts b/lib/routes/javtiful/utils.ts
new file mode 100644
index 00000000000000..e3fe4c5850da04
--- /dev/null
+++ b/lib/routes/javtiful/utils.ts
@@ -0,0 +1,18 @@
+import { getCurrentPath } from '@/utils/helpers';
+const __dirname = getCurrentPath(import.meta.url);
+
+import { art } from '@/utils/render';
+import path from 'node:path';
+import { parseRelativeDate } from '@/utils/parse-date';
+
+const renderDescription = (data) => art(path.join(__dirname, 'templates/description.art'), data);
+
+export const parseItems = (e) => ({
+ title: e.find('a > img').attr('alt')!,
+ link: e.find('a').attr('href')!,
+ description: renderDescription({
+ poster: e.find('a > img').data('src'),
+ previewVideo: e.find('a > span').data('trailer'),
+ }),
+ pubDate: parseRelativeDate(e.find('.video-addtime').text()),
+});
diff --git a/lib/routes/javtrailers/casts.ts b/lib/routes/javtrailers/casts.ts
new file mode 100644
index 00000000000000..f8c3bceca40040
--- /dev/null
+++ b/lib/routes/javtrailers/casts.ts
@@ -0,0 +1,41 @@
+import { Route } from '@/types';
+
+import ofetch from '@/utils/ofetch';
+import cache from '@/utils/cache';
+import { baseUrl, getItem, headers, parseList } from './utils';
+
+export const route: Route = {
+ path: '/casts/:cast',
+ categories: ['multimedia'],
+ example: '/javtrailers/casts/hibiki-otsuki',
+ parameters: { cast: 'Cast name, can be found in the URL of the cast page' },
+ radar: [
+ {
+ source: ['javtrailers.com/casts/:category'],
+ },
+ ],
+ name: 'Casts',
+ maintainers: ['TonyRL'],
+ url: 'javtrailers.com/casts',
+ handler,
+};
+
+async function handler(ctx) {
+ const { cast } = ctx.req.param();
+
+ const response = await ofetch(`${baseUrl}/api/casts/${cast}?page=0`, {
+ headers,
+ });
+
+ const list = parseList(response.videos);
+
+ const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => getItem(item))));
+
+ return {
+ title: `Watch ${response.cast.name} Jav Online | Japanese Adult Video - JavTrailers.com`,
+ description: response.cast.castWiki?.description.replaceAll('\n', ' ') ?? `Watch ${response.cast.name} Jav video’s free, we have the largest Jav collections with high definition`,
+ image: response.cast.avatar,
+ link: `${baseUrl}/casts/${cast}`,
+ item: items,
+ };
+}
diff --git a/lib/routes/javtrailers/categories.ts b/lib/routes/javtrailers/categories.ts
new file mode 100644
index 00000000000000..402e366b3a2292
--- /dev/null
+++ b/lib/routes/javtrailers/categories.ts
@@ -0,0 +1,40 @@
+import { Route } from '@/types';
+
+import ofetch from '@/utils/ofetch';
+import cache from '@/utils/cache';
+import { baseUrl, getItem, headers, parseList } from './utils';
+
+export const route: Route = {
+ path: '/categories/:category',
+ categories: ['multimedia'],
+ example: '/javtrailers/categories/50001755',
+ parameters: { category: 'Category name, can be found in the URL of the category page' },
+ radar: [
+ {
+ source: ['javtrailers.com/categories/:category'],
+ },
+ ],
+ name: 'Categories',
+ maintainers: ['TonyRL'],
+ url: 'javtrailers.com/categories',
+ handler,
+};
+
+async function handler(ctx) {
+ const { category } = ctx.req.param();
+
+ const response = await ofetch(`${baseUrl}/api/categories/${category}?page=0`, {
+ headers,
+ });
+
+ const list = parseList(response.videos);
+
+ const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => getItem(item))));
+
+ return {
+ title: `Watch ${response.category.name} Jav Online | Japanese Adult Video - JavTrailers.com`,
+ description: `Watch ${response.category.name} Jav video’s free, we have the largest Jav collections with high definition`,
+ link: `${baseUrl}/categories/${category}`,
+ item: items,
+ };
+}
diff --git a/lib/routes/javtrailers/namespace.ts b/lib/routes/javtrailers/namespace.ts
new file mode 100644
index 00000000000000..48e5c45912fb15
--- /dev/null
+++ b/lib/routes/javtrailers/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'JavTrailers',
+ url: 'javtrailers.com',
+ lang: 'ja',
+};
diff --git a/lib/routes/javtrailers/studios.ts b/lib/routes/javtrailers/studios.ts
new file mode 100644
index 00000000000000..72ebfca6c6d881
--- /dev/null
+++ b/lib/routes/javtrailers/studios.ts
@@ -0,0 +1,39 @@
+import { Route } from '@/types';
+
+import ofetch from '@/utils/ofetch';
+import cache from '@/utils/cache';
+import { baseUrl, getItem, headers, parseList } from './utils';
+
+export const route: Route = {
+ path: '/studios/:studio',
+ categories: ['multimedia'],
+ example: '/javtrailers/studios/s1-no-1-style',
+ parameters: { studio: 'Studio name, can be found in the URL of the studio page' },
+ radar: [
+ {
+ source: ['javtrailers.com/studios/:category'],
+ },
+ ],
+ name: 'Studios',
+ maintainers: ['TonyRL'],
+ handler,
+};
+
+async function handler(ctx) {
+ const { studio } = ctx.req.param();
+
+ const response = await ofetch(`${baseUrl}/api/studios/${studio}?page=0`, {
+ headers,
+ });
+
+ const list = parseList(response.videos);
+
+ const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => getItem(item))));
+
+ return {
+ title: `${response.studio.hotDvdIds?.join(' ') ?? response.studio.name} Jav Online | Japanese Adult Video - JavTrailers.com`,
+ description: 'Watch Jav made by Prestige free, with high definition, we have over 4,000 studios available for free streaming.',
+ link: `${baseUrl}/studios/${studio}`,
+ item: items,
+ };
+}
diff --git a/lib/routes/javtrailers/templates/description.art b/lib/routes/javtrailers/templates/description.art
new file mode 100644
index 00000000000000..36b47a10240a94
--- /dev/null
+++ b/lib/routes/javtrailers/templates/description.art
@@ -0,0 +1,31 @@
+{{ if videoInfo.image }}
+
+{{ /if }}
+
+{{ if videoInfo.dvdId }}DVD ID: {{ videoInfo.dvdId }}
{{ /if }}
+{{ if videoInfo.contentId }}Content ID: {{ videoInfo.contentId }}
{{ /if }}
+{{ if videoInfo.releaseDate }}Release Date: {{ videoInfo.releaseDate }}
{{ /if }}
+{{ if videoInfo.duration }}Duration: {{ videoInfo.duration }} mins
{{ /if }}
+{{ if videoInfo.director }}Director: {{ videoInfo.director }} {{ videoInfo.jpDirector }}
{{ /if }}
+{{ if videoInfo.studio }}Studio: {{ videoInfo.studio.name }}
{{ /if }}
+{{ if videoInfo.categories }}
+ Categories:
+ {{ each videoInfo.categories c }}
+ {{ c.name }},
+ {{ /each }}
+
+{{ /if }}
+{{ if videoInfo.casts }}
+ Cast(s):
+ {{ each videoInfo.casts c }}
+ {{ c.name }} {{ c.jpName }}
+ {{ /each }}
+
+{{ /if }}
+
+
+{{ if videoInfo.gallery }}
+ {{ each videoInfo.gallery g }}
+
+ {{ /each }}
+{{ /if }}
diff --git a/lib/routes/javtrailers/types.ts b/lib/routes/javtrailers/types.ts
new file mode 100644
index 00000000000000..2ac2e1eb4c5403
--- /dev/null
+++ b/lib/routes/javtrailers/types.ts
@@ -0,0 +1,59 @@
+export interface Video {
+ _id: string;
+ categories: Category[];
+ casts: Cast[];
+ director: string;
+ gallery: string[];
+ title: string;
+ javLink: JavLink;
+ contentId: string;
+ dvdId: string;
+ studio: Studio;
+ releaseDate: string;
+ duration: number;
+ image: string;
+ jpDirector: string;
+ jpTitle: string;
+ /**
+ * HLS stream URL
+ */
+ trailer: string;
+ zhTitle: string;
+ __v: number;
+}
+
+interface Category {
+ _id: string;
+ slug: string;
+ name: string;
+ jpName: string;
+ zhName: string;
+}
+
+interface Cast {
+ _id: string;
+ slug: string;
+ ruby: string;
+ link: string;
+ name: string;
+ jpName: string;
+ avatar: string;
+ __v: number;
+}
+
+interface JavLink {
+ _id: string;
+ link: string;
+ processed: boolean;
+ isProfessional: boolean;
+ upcoming: boolean;
+ __v: number;
+}
+
+interface Studio {
+ _id: string;
+ slug: string;
+ name: string;
+ link: string;
+ jpName: string;
+}
diff --git a/lib/routes/javtrailers/utils.ts b/lib/routes/javtrailers/utils.ts
new file mode 100644
index 00000000000000..5b8037bef24240
--- /dev/null
+++ b/lib/routes/javtrailers/utils.ts
@@ -0,0 +1,48 @@
+import { Video } from './types';
+
+import ofetch from '@/utils/ofetch';
+import { parseDate } from '@/utils/parse-date';
+import { art } from '@/utils/render';
+import path from 'node:path';
+import { getCurrentPath } from '@/utils/helpers';
+const __dirname = getCurrentPath(import.meta.url);
+
+export const baseUrl = 'https://javtrailers.com';
+export const headers = {
+ Authorization: 'AELAbPQCh_fifd93wMvf_kxMD_fqkUAVf@BVgb2!md@TNW8bUEopFExyGCoKRcZX',
+};
+
+export const hdGallery = (gallery) =>
+ gallery.map((item) => {
+ if (item.startsWith('https://pics.dmm.co.jp/')) {
+ return item.replace(/-(\d+)\.jpg$/, 'jp-$1.jpg');
+ } else if (item.startsWith('https://image.mgstage.com/')) {
+ return item.replace(/cap_t1_/, 'cap_e_');
+ }
+ return item;
+ });
+
+export const parseList = (videos) =>
+ videos.map((item) => ({
+ title: `${item.dvdId} ${item.title}`,
+ link: `${baseUrl}/video/${item.contentId}`,
+ pubDate: parseDate(item.releaseDate),
+ contentId: item.contentId,
+ }));
+
+export const getItem = async (item) => {
+ const response = await ofetch(`${baseUrl}/api/video/${item.contentId}`, {
+ headers,
+ });
+
+ const videoInfo: Video = response.video;
+ videoInfo.gallery = hdGallery(videoInfo.gallery);
+
+ item.description = art(path.join(__dirname, 'templates/description.art'), {
+ videoInfo,
+ });
+ item.author = videoInfo.casts.map((cast) => `${cast.name} ${cast.jpName}`).join(', ');
+ item.category = videoInfo.categories.map((category) => `${category.name}/${category.jpName}/${category.zhName}`);
+
+ return item;
+};
diff --git a/lib/routes/jd/namespace.ts b/lib/routes/jd/namespace.ts
index 9bd813e67704c2..c1a6be51238fba 100644
--- a/lib/routes/jd/namespace.ts
+++ b/lib/routes/jd/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '京东',
url: 'item.jd.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/jd/price.ts b/lib/routes/jd/price.ts
index 9a78c18e9b8730..4e44573b309b41 100644
--- a/lib/routes/jd/price.ts
+++ b/lib/routes/jd/price.ts
@@ -22,9 +22,9 @@ export const route: Route = {
name: '商品价格',
maintainers: ['nczitzk'],
handler,
- description: `:::tip
+ description: `::: tip
如商品 \`https://item.jd.com/526835.html\` 中的 id 为 \`526835\`,所以路由为 [\`/jd/price/526835\`](https://rsshub.app/jd/price/526835)
- :::`,
+:::`,
};
async function handler(ctx) {
diff --git a/lib/routes/jewishmuseum/namespace.ts b/lib/routes/jewishmuseum/namespace.ts
index 219e8e016e9d59..0efe1aefb03c1f 100644
--- a/lib/routes/jewishmuseum/namespace.ts
+++ b/lib/routes/jewishmuseum/namespace.ts
@@ -1,6 +1,7 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
- name: '纽约犹太人博物馆',
+ name: 'The Jewish Museum',
url: 'thejewishmuseum.org',
+ lang: 'en',
};
diff --git a/lib/routes/jianshu/collection.ts b/lib/routes/jianshu/collection.ts
index da3481dc3904af..96edd87c04cd87 100644
--- a/lib/routes/jianshu/collection.ts
+++ b/lib/routes/jianshu/collection.ts
@@ -1,4 +1,4 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
@@ -6,7 +6,8 @@ import util from './utils';
export const route: Route = {
path: '/collection/:id',
- categories: ['social-media'],
+ categories: ['social-media', 'popular'],
+ view: ViewType.Articles,
example: '/jianshu/collection/xYuZYD',
parameters: { id: '专题 id, 可在专题页 URL 中找到' },
features: {
diff --git a/lib/routes/jianshu/home.ts b/lib/routes/jianshu/home.ts
index ca8ba4cf5b3a06..a2b208a92d260c 100644
--- a/lib/routes/jianshu/home.ts
+++ b/lib/routes/jianshu/home.ts
@@ -1,4 +1,4 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
@@ -6,7 +6,8 @@ import util from './utils';
export const route: Route = {
path: '/home',
- categories: ['social-media'],
+ categories: ['social-media', 'popular'],
+ view: ViewType.Articles,
example: '/jianshu/home',
parameters: {},
features: {
diff --git a/lib/routes/jianshu/namespace.ts b/lib/routes/jianshu/namespace.ts
index 227f6fe8374048..adffa15ffe7d48 100644
--- a/lib/routes/jianshu/namespace.ts
+++ b/lib/routes/jianshu/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '简书',
url: 'www.jianshu.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/jianshu/user.ts b/lib/routes/jianshu/user.ts
index 0dd95a511060f4..1f3d4228d69c4e 100644
--- a/lib/routes/jianshu/user.ts
+++ b/lib/routes/jianshu/user.ts
@@ -1,4 +1,4 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { load } from 'cheerio';
@@ -6,8 +6,9 @@ import util from './utils';
export const route: Route = {
path: '/user/:id',
- categories: ['social-media'],
+ categories: ['social-media', 'popular'],
example: '/jianshu/user/yZq3ZV',
+ view: ViewType.Articles,
parameters: { id: '作者 id, 可在作者主页 URL 中找到' },
features: {
requireConfig: false,
diff --git a/lib/routes/jiaoliudao/namespace.ts b/lib/routes/jiaoliudao/namespace.ts
index 74266cda6aa26c..a4e28c03ce7c12 100644
--- a/lib/routes/jiaoliudao/namespace.ts
+++ b/lib/routes/jiaoliudao/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '交流岛资源网',
url: 'jiaoliudao.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/jiemian/list.ts b/lib/routes/jiemian/list.ts
index ea316e70148732..95637ee14fd901 100644
--- a/lib/routes/jiemian/list.ts
+++ b/lib/routes/jiemian/list.ts
@@ -10,5 +10,5 @@ function handler(ctx) {
const id = ctx.req.param('id');
const redirectTo = `/jiemian${id ? `/lists/${id}` : ''}`;
- ctx.redirect(redirectTo);
+ ctx.set('redirect', redirectTo);
}
diff --git a/lib/routes/jiemian/namespace.ts b/lib/routes/jiemian/namespace.ts
index 8e8c3afbb0765b..0db74534281fa1 100644
--- a/lib/routes/jiemian/namespace.ts
+++ b/lib/routes/jiemian/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '界面新闻',
url: 'jiemian.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/jike/namespace.ts b/lib/routes/jike/namespace.ts
index 2b5542a2cc0425..ee82203acc8120 100644
--- a/lib/routes/jike/namespace.ts
+++ b/lib/routes/jike/namespace.ts
@@ -3,4 +3,5 @@ import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: '即刻',
url: 'm.okjike.com',
+ lang: 'zh-CN',
};
diff --git a/lib/routes/jike/topic.ts b/lib/routes/jike/topic.ts
index d1b84bd0b91520..b4d401b0df09bf 100644
--- a/lib/routes/jike/topic.ts
+++ b/lib/routes/jike/topic.ts
@@ -1,4 +1,4 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { topicDataHanding, constructTopicEntry } from './utils';
@@ -9,9 +9,16 @@ const urlRegex = /(https?:\/\/[^\s"'<>]+)/g;
export const route: Route = {
path: '/topic/:id/:showUid?',
- categories: ['social-media'],
+ categories: ['social-media', 'popular'],
+ view: ViewType.SocialMedia,
example: '/jike/topic/556688fae4b00c57d9dd46ee',
- parameters: { id: '圈子 id, 可在即刻 web 端圈子页或 APP 分享出来的圈子页 URL 中找到', showUid: '是否在内容中显示用户信息,设置为 1 则开启' },
+ parameters: {
+ id: '圈子 id, 可在即刻 web 端圈子页或 APP 分享出来的圈子页 URL 中找到',
+ showUid: {
+ description: '是否在内容中显示用户信息,设置为 1 则开启',
+ options: [{ value: '1', label: '显示' }],
+ },
+ },
features: {
requireConfig: false,
requirePuppeteer: false,
diff --git a/lib/routes/jike/user.ts b/lib/routes/jike/user.ts
index 42441a9ac0925c..7e2a3bd9448963 100644
--- a/lib/routes/jike/user.ts
+++ b/lib/routes/jike/user.ts
@@ -1,11 +1,12 @@
-import { Route } from '@/types';
+import { Route, ViewType } from '@/types';
import got from '@/utils/got';
import { load } from 'cheerio';
import { parseDate } from '@/utils/parse-date';
export const route: Route = {
path: '/user/:id',
- categories: ['social-media'],
+ categories: ['social-media', 'popular'],
+ view: ViewType.SocialMedia,
example: '/jike/user/3EE02BC9-C5B3-4209-8750-4ED1EE0F67BB',
parameters: { id: '用户 id, 可在即刻分享出来的单条动态页点击用户头像进入个人主页,然后在个人主页的 URL 中找到,或者在单条动态页使用 RSSHub Radar 插件' },
features: {
diff --git a/lib/routes/jimmyspa/books.ts b/lib/routes/jimmyspa/books.ts
new file mode 100644
index 00000000000000..d925acd216816f
--- /dev/null
+++ b/lib/routes/jimmyspa/books.ts
@@ -0,0 +1,112 @@
+import { Route, ViewType } from '@/types';
+import { parseDate } from '@/utils/parse-date';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { art } from '@/utils/render';
+import { getCurrentPath } from '@/utils/helpers';
+import cache from '@/utils/cache';
+import path from 'node:path';
+
+const __dirname = getCurrentPath(import.meta.url);
+export const route: Route = {
+ path: '/books/:language',
+ categories: ['design'],
+ view: ViewType.Articles,
+ example: '/jimmyspa/books/tw',
+ parameters: {
+ language: {
+ description: '语言',
+ options: [
+ { value: 'tw', label: '臺灣正體' },
+ { value: 'en', label: 'English' },
+ { value: 'jp', label: '日本語' },
+ ],
+ },
+ },
+ radar: [
+ {
+ source: ['www.jimmyspa.com/:language/Books'],
+ },
+ ],
+ name: 'Books',
+ description: `
+| language | Description |
+| --- | --- |
+| tw | 臺灣正體 |
+| en | English |
+| jp | 日本語 |
+ `,
+ maintainers: ['FYLSen'],
+ handler,
+};
+
+async function handler(ctx) {
+ const language = ctx.req.param('language');
+ const baseUrl = 'https://www.jimmyspa.com';
+ const booksListUrl = new URL(`/${language}/Books/Ajax/changeList?year=&keyword=&categoryId=0&page=1`, baseUrl).href;
+
+ const listResponse = await got(booksListUrl);
+ const listPage = load(listResponse.data.view);
+
+ const bookItems = listPage('ul#appendWork li.work_block')
+ .toArray()
+ .map(async (bookElement) => {
+ const bookContent = load(bookElement);
+ const bookTitle = bookContent('p.tit').text();
+ const bookImageRelative = bookContent('div.work_img img').prop('src') || '';
+ const bookImageUrl = bookImageRelative ? baseUrl + bookImageRelative : '';
+ const bookDetailUrl = bookContent('li.work_block').prop('data-route');
+
+ const { renderedDescription, publishDate } = (await cache.tryGet(bookDetailUrl, async () => {
+ const detailResponse = await got(bookDetailUrl);
+ const detailPage = load(detailResponse.data);
+ const bookDescription = detailPage('article.intro_cont').html() || '';
+ const bookInfoWrap = detailPage('div.info_wrap').html() || '';
+
+ const processedDescription = bookDescription.replaceAll(/]*>/g, (imgTag) =>
+ imgTag.replaceAll(/\b(src|data-src)="(?!http|https|\/\/)([^"]*)"/g, (_, attrName, relativePath) => {
+ const absoluteImageUrl = new URL(relativePath, baseUrl).href;
+ return `${attrName}="${absoluteImageUrl}"`;
+ })
+ );
+
+ const publishDateMatch = bookInfoWrap.match(/(首次出版|First Published|初版)<\/span>\s*([^<]+)<\/span>/);
+ const publishDate = publishDateMatch ? parseDate(publishDateMatch[2] + '-02') : '';
+
+ const renderedDescription = art(path.join(__dirname, 'templates/description.art'), {
+ images: bookImageUrl
+ ? [
+ {
+ src: bookImageUrl,
+ alt: bookTitle,
+ },
+ ]
+ : undefined,
+ description: processedDescription,
+ });
+
+ return {
+ renderedDescription,
+ publishDate,
+ };
+ })) as { renderedDescription: string; publishDate: string };
+
+ return {
+ title: bookTitle,
+ link: bookDetailUrl,
+ description: renderedDescription,
+ pubDate: publishDate,
+ content: {
+ html: renderedDescription,
+ text: bookTitle,
+ },
+ };
+ });
+
+ return {
+ title: `幾米 - 幾米創作(${language})`,
+ link: `${baseUrl}/${language}/Books`,
+ allowEmpty: true,
+ item: await Promise.all(bookItems),
+ };
+}
diff --git a/lib/routes/jimmyspa/namespace.ts b/lib/routes/jimmyspa/namespace.ts
new file mode 100644
index 00000000000000..2b92c8958c87ea
--- /dev/null
+++ b/lib/routes/jimmyspa/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: '幾米 JIMMY S.P.A. Official Website',
+ url: 'www.jimmyspa.com',
+ lang: 'zh-TW',
+};
diff --git a/lib/routes/jimmyspa/news.ts b/lib/routes/jimmyspa/news.ts
new file mode 100644
index 00000000000000..5d7788640f4dc0
--- /dev/null
+++ b/lib/routes/jimmyspa/news.ts
@@ -0,0 +1,124 @@
+import { Route, ViewType } from '@/types';
+import { parseDate } from '@/utils/parse-date';
+import got from '@/utils/got';
+import { load } from 'cheerio';
+import { art } from '@/utils/render';
+import { getCurrentPath } from '@/utils/helpers';
+const __dirname = getCurrentPath(import.meta.url);
+import path from 'node:path';
+
+export const route: Route = {
+ path: '/news/:language',
+ categories: ['design'],
+ view: ViewType.Pictures,
+ example: '/jimmyspa/news/tw',
+ parameters: {
+ language: {
+ description: '语言',
+ options: [
+ { value: 'tw', label: '臺灣正體' },
+ { value: 'en', label: 'English' },
+ { value: 'jp', label: '日本語' },
+ ],
+ },
+ },
+ radar: [
+ {
+ source: ['www.jimmyspa.com/:language/News'],
+ },
+ ],
+ name: 'News',
+ description: `
+| language | Description |
+| --- | --- |
+| tw | 臺灣正體 |
+| en | English |
+| jp | 日本語 |
+ `,
+ maintainers: ['FYLSen'],
+ handler,
+};
+
+async function handler(ctx) {
+ const language = ctx.req.param('language');
+ const rootUrl = 'https://www.jimmyspa.com';
+
+ const currentUrl = new URL(`/${language}/News/Ajax/changeList?year=&keyword=&categoryId=0&page=1`, rootUrl).href;
+
+ const responseData = await got(currentUrl);
+
+ const $ = load(responseData.data.view);
+
+ const items = $('ul#appendNews li.card_block')
+ .toArray()
+ .map((item) => {
+ const $$ = load(item);
+ const title = $$('a.news_card .info_wrap h3').text();
+ const image = $$('a.news_card .card_img img').prop('src') || '';
+ const link = $$('a.news_card').prop('data-route');
+ const itemdate = $$('a.news_card div.date').html() || '';
+ const pubDate = convertHtmlDateToStandardFormat(itemdate.toString());
+
+ const description = art(path.join(__dirname, 'templates/description.art'), {
+ images: image
+ ? [
+ {
+ src: image,
+ alt: title,
+ },
+ ]
+ : undefined,
+ description: $$('a.news_card .info_wrap p').text(),
+ });
+
+ return {
+ title,
+ link,
+ description,
+ pubDate,
+ content: {
+ html: description,
+ text: title,
+ },
+ };
+ });
+
+ return {
+ title: `幾米 - 最新消息(${language})`,
+ link: `${rootUrl}/${language}/News`,
+ allowEmpty: true,
+ item: items,
+ };
+}
+
+function convertHtmlDateToStandardFormat(htmlContent: string): Date | undefined {
+ const dateRegex = /
+